From 29ff42d15fb8bf33e4e18259e27bb764682e153c Mon Sep 17 00:00:00 2001 From: Kamile Demir Date: Fri, 19 Feb 2021 17:17:28 -0500 Subject: [PATCH] Reusing otlLib buildStatTable() in feaLib --- Lib/fontTools/feaLib/ast.py | 28 ++- Lib/fontTools/feaLib/builder.py | 174 +++++--------- Lib/fontTools/feaLib/parser.py | 10 +- Lib/fontTools/otlLib/builder.py | 17 +- Tests/feaLib/builder_test.py | 5 +- Tests/feaLib/data/STAT_bad.fea | 3 +- Tests/feaLib/data/STAT_test.ttx | 88 +++---- .../data/STAT_test_elidedFallbackNameID.fea | 84 +++++++ .../data/STAT_test_elidedFallbackNameID.ttx | 225 ++++++++++++++++++ Tests/feaLib/parser_test.py | 6 +- 10 files changed, 453 insertions(+), 187 deletions(-) create mode 100644 Tests/feaLib/data/STAT_test_elidedFallbackNameID.fea create mode 100644 Tests/feaLib/data/STAT_test_elidedFallbackNameID.ttx diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 949d318aa..53b100ec7 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -29,7 +29,7 @@ __all__ = [ "Anchor", "AnchorDefinition", "AttachStatement", - "AxisValueLocation", + "AxisValueLocationStatement", "BaseAxis", "CVParametersNameStatement", "ChainContextPosStatement", @@ -66,8 +66,8 @@ __all__ = [ "SingleSubstStatement", "SizeParameters", "Statement", - "STATAxisValueRecord", - "STATDesignAxis", + "STATAxisValueStatement", + "STATDesignAxisStatement", "STATNameStatement", "SubtableStatement", "TableBlock", @@ -1902,7 +1902,7 @@ class VheaField(Statement): return "{} {};".format(keywords[self.key], self.value) -class STATDesignAxis(Statement): +class STATDesignAxisStatement(Statement): """A STAT table Design Axis Args: @@ -1970,12 +1970,12 @@ class ElidedFallbackNameID(Statement): return f"ElidedFallbackNameID {self.value};" -class STATAxisValueRecord(Statement): +class STATAxisValueStatement(Statement): """A STAT table Axis Value Record Args: names (list): a list of :class:`STATNameStatement` objects - locations (list): a list of :class:`AxisValueLocation` objects + locations (list): a list of :class:`AxisValueLocationStatement` objects flags (int): an int """ def __init__(self, names, locations, flags, location=None): @@ -1990,8 +1990,7 @@ class STATAxisValueRecord(Statement): def asFea(self, indent=""): res = "AxisValue {\n" for location in self.locations: - res += f"location {location.tag} " - res += f"{' '.join(str(i) for i in location.values)};\n" + res += location.asFea() for nameRecord in self.names: res += nameRecord.asFea() @@ -2010,7 +2009,7 @@ class STATAxisValueRecord(Statement): return res -class AxisValueLocation(NamedTuple): +class AxisValueLocationStatement(Statement): """ A STAT table Axis Value Location @@ -2018,5 +2017,12 @@ class AxisValueLocation(NamedTuple): tag (str): a 4 letter axis tag values (list): a list of ints and/or floats """ - tag: str - values: list + def __init__(self, tag, values, location=None): + Statement.__init__(self, location) + self.tag = tag + self.values = values + + def asFea(self, res=""): + res += f"location {self.tag} " + res += f"{' '.join(str(i) for i in self.values)};\n" + return res diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index e269a4d4f..ff03b79ab 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -536,7 +536,7 @@ class Builder(object): self.stat_["DesignAxes"] = [] if designAxis.tag in (r.tag for r in self.stat_["DesignAxes"]): raise FeatureLibError( - 'DesignAxis already defined for tag "%s".' % designAxis.tag, + f'DesignAxis already defined for tag "{designAxis.tag}".', location, ) if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]): @@ -551,10 +551,11 @@ class Builder(object): self.stat_["AxisValueRecords"] = [] # Check for duplicate AxisValueRecords for record_ in self.stat_["AxisValueRecords"]: - if (sorted([n.asFea() for n in record_.names]) == - sorted([n.asFea() for n in axisValueRecord.names]) and - sorted(record_.locations) == sorted(axisValueRecord.locations) - and record_.flags == axisValueRecord.flags): + if ({n.asFea() for n in record_.names} == + {n.asFea() for n in axisValueRecord.names} and + {n.asFea() for n in record_.locations} == + {n.asFea() for n in axisValueRecord.locations} + and record_.flags == axisValueRecord.flags): raise FeatureLibError( "An AxisValueRecord with these values is already defined.", location, @@ -564,126 +565,65 @@ class Builder(object): def build_STAT(self): if not self.stat_: return - self.font["STAT"] = newTable("STAT") - table = self.font["STAT"].table = otTables.STAT() - table.Version = 0x00010001 + + axes = self.stat_.get("DesignAxes") + if not axes: + raise FeatureLibError('DesignAxes not defined', None) + axisValueRecords = self.stat_.get("AxisValueRecords") + axisValues = {} + format4_locations = [] + for tag in axes: + axisValues[tag.tag] = [] + if axisValueRecords is not None: + for avr in axisValueRecords: + valuesDict = {} + if avr.flags > 0: + valuesDict['flags'] = avr.flags + if len(avr.locations) == 1: + location = avr.locations[0] + values = location.values + if len(values) == 1: #format1 + valuesDict.update({'value': values[0],'name': avr.names}) + if len(values) == 2: #format3 + valuesDict.update({ 'value': values[0], + 'linkedValue': values[1], + 'name': avr.names}) + if len(values) == 3: #format2 + nominal, minVal, maxVal = values + valuesDict.update({ 'nominalValue': nominal, + 'rangeMinValue': minVal, + 'rangeMaxValue': maxVal, + 'name': avr.names}) + axisValues[location.tag].append(valuesDict) + else: + valuesDict.update({"location": {i.tag: i.values[0] + for i in avr.locations}, + "name": avr.names}) + format4_locations.append(valuesDict) + + designAxes = [{"ordering": a.axisOrder, + "tag": a.tag, + "name": a.names, + 'values': axisValues[a.tag]} for a in axes] + nameTable = self.font.get("name") if not nameTable: # this only happens for unit tests nameTable = self.font["name"] = newTable("name") nameTable.names = [] + if "ElidedFallbackNameID" in self.stat_: - nameID = self.stat_["ElidedFallbackNameID"] - name = nameTable.getDebugName(nameID) + nameID = self.stat_["ElidedFallbackNameID"] + name = nameTable.getDebugName(nameID) if not name: - raise FeatureLibError('ElidedFallbackNameID %d points ' + raise FeatureLibError(f'ElidedFallbackNameID {nameID} points ' 'to a nameID that does not exist in the ' - '"name" table' % nameID, None) - table.ElidedFallbackNameID = nameID - if "ElidedFallbackName" in self.stat_: - nameRecords = self.stat_["ElidedFallbackName"] - nameID = self.get_user_name_id(nameTable) - for nameRecord in nameRecords: - nameTable.setName(nameRecord.string, nameID, - nameRecord.platformID, nameRecord.platEncID, - nameRecord.langID) - table.ElidedFallbackNameID = nameID + '"name" table', None) + elif "ElidedFallbackName" in self.stat_: + nameID = self.stat_["ElidedFallbackName"] + + otl.buildStatTable(self.font, designAxes, locations=format4_locations, + elidedFallbackName=nameID) - axisRecords = [] - axisValueRecords = [] - designAxisOrder = {} - for record in self.stat_["DesignAxes"]: - axis = otTables.AxisRecord() - axis.AxisTag = record.tag - nameID = self.get_user_name_id(nameTable) - for nameRecord in record.names: - nameTable.setName(nameRecord.string, nameID, - nameRecord.platformID, nameRecord.platEncID, - nameRecord.langID) - - axis.AxisNameID = nameID - axis.AxisOrdering = record.axisOrder - axisRecords.append(axis) - designAxisOrder[record.tag] = record.axisOrder - - if "AxisValueRecords" in self.stat_: - for record in self.stat_["AxisValueRecords"]: - if len(record.locations) == 1: - location = record.locations[0] - tag = location.tag - values = location.values - axisOrder = designAxisOrder[tag] - axisValueRecord = otTables.AxisValue() - axisValueRecord.AxisIndex = axisOrder - axisValueRecord.Flags = record.flags - - nameID = self.get_user_name_id(nameTable) - for nameRecord in record.names: - nameTable.setName(nameRecord.string, nameID, - nameRecord.platformID, - nameRecord.platEncID, - nameRecord.langID) - - axisValueRecord.ValueNameID = nameID - - if len(values) == 1: - axisValueRecord.Format = 1 - axisValueRecord.Value = values[0] - if len(values) == 2: - axisValueRecord.Format = 3 - axisValueRecord.Value = values[0] - axisValueRecord.LinkedValue = values[1] - if len(values) == 3: - axisValueRecord.Format = 2 - nominal, minVal, maxVal = values - axisValueRecord.NominalValue = nominal - axisValueRecord.RangeMinValue = minVal - axisValueRecord.RangeMaxValue = maxVal - axisValueRecords.append(axisValueRecord) - - if len(record.locations) > 1: - # Multiple locations = Format 4 - table.Version = 0x00010002 - axisValue = otTables.AxisValue() - axisValue.Format = 4 - - nameID = self.get_user_name_id(nameTable) - for nameRecord in record.names: - nameTable.setName(nameRecord.string, nameID, - nameRecord.platformID, - nameRecord.platEncID, - nameRecord.langID) - - axisValue.ValueNameID = nameID - axisValue.Flags = record.flags - - axisValueRecords_fmt4 = [] - for location in record.locations: - tag = location.tag - values = location.values - axisOrder = designAxisOrder[tag] - axisValueRecord = otTables.AxisValueRecord() - axisValueRecord.AxisIndex = axisOrder - axisValueRecord.Value = values[0] - axisValueRecords_fmt4.append(axisValueRecord) - axisValue.AxisCount = len(axisValueRecords_fmt4) - axisValue.AxisValueRecord = axisValueRecords_fmt4 - axisValueRecords.append(axisValue) - - if axisRecords: - # Store AxisRecords - axisRecordArray = otTables.AxisRecordArray() - axisRecordArray.Axis = axisRecords - # XXX these should not be hard-coded but computed automatically - table.DesignAxisRecordSize = 8 - table.DesignAxisRecord = axisRecordArray - table.DesignAxisCount = len(axisRecords) - - if axisValueRecords: - # Store AxisValueRecords - axisValueArray = otTables.AxisValueArray() - axisValueArray.AxisValue = axisValueRecords - table.AxisValueArray = axisValueArray - table.AxisValueCount = len(axisValueRecords) def build_codepages_(self, pages): pages2bits = { diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index ff2330f84..f638c68c4 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -1186,8 +1186,6 @@ class Parser(object): langID = langID or 0x0409 # English string = self.expect_string_() - # self.expect_symbol_(";") - encoding = getEncoding(platformID, platEncID, langID) if encoding is None: raise FeatureLibError("Unsupported encoding", location) @@ -1368,7 +1366,7 @@ class Parser(object): self.cur_token_location_) self.expect_symbol_("}") - return self.ast.STATDesignAxis(axisTag, axisOrder, names, self.cur_token_location_) + return self.ast.STATDesignAxisStatement(axisTag, axisOrder, names, self.cur_token_location_) def parse_STAT_axis_value_(self): assert self.is_cur_keyword_("AxisValue") @@ -1420,7 +1418,7 @@ class Parser(object): self.cur_token_location_) format4_tags.append(tag) - return self.ast.STATAxisValueRecord(names, locations, flags, self.cur_token_location_) + return self.ast.STATAxisValueStatement(names, locations, flags, self.cur_token_location_) def parse_STAT_location(self): values = [] @@ -1447,7 +1445,7 @@ class Parser(object): f'of specified range ' f'{min_val}-{max_val}.', self.next_token_location_) - return self.ast.AxisValueLocation(tag, values) + return self.ast.AxisValueLocationStatement(tag, values) def parse_table_STAT_(self, table): statements = table.statements @@ -1989,7 +1987,7 @@ class Parser(object): raise FeatureLibError("Expected a tag", self.cur_token_location_) if len(self.cur_token_) > 4: raise FeatureLibError( - "Tags can not be longer than 4 characters", self.cur_token_location_ + "Tags cannot be longer than 4 characters", self.cur_token_location_ ) return (self.cur_token_ + " ")[:4] diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 029aa3fc5..ab27463a6 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -9,6 +9,7 @@ from fontTools.ttLib.tables.otBase import ( CountReference, ) from fontTools.ttLib.tables import otBase +from fontTools.feaLib.ast import STATNameStatement from fontTools.otlLib.error import OpenTypeLibError import logging import copy @@ -2687,8 +2688,8 @@ def buildStatTable(ttFont, axes, locations=None, elidedFallbackName=2): ] The optional 'elidedFallbackName' argument can be a name ID (int), - a string, or a dictionary containing multilingual names. It - translates to the ElidedFallbackNameID field. + a string, a dictionary containing multilingual names, or a list of + STATNameStatements. It translates to the ElidedFallbackNameID field. The 'ttFont' argument must be a TTFont instance that already has a 'name' table. If a 'STAT' table already exists, it will be @@ -2797,6 +2798,16 @@ def _addName(nameTable, value, minNameID=0): names = dict(en=value) elif isinstance(value, dict): names = value + elif isinstance(value, list): + nameID = nameTable._findUnusedNameID() + for nameRecord in value: + if isinstance(nameRecord, STATNameStatement): + nameTable.setName(nameRecord.string, + nameID,nameRecord.platformID, + nameRecord.platEncID,nameRecord.langID) + else: + raise TypeError("value must be a list of STATNameStatements") + return nameID else: - raise TypeError("value must be int, str or dict") + raise TypeError("value must be int, str, dict or list") return nameTable.addMultilingualName(names, minNameID=minNameID) diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 6c1a664ae..2f6319e62 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -74,7 +74,7 @@ class BuilderTest(unittest.TestCase): LigatureSubtable AlternateSubtable MultipleSubstSubtable SingleSubstSubtable aalt_chain_contextual_subst AlternateChained MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats - GSUB_5_formats delete_glyph STAT_test + GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID """.split() def __init__(self, methodName): @@ -514,6 +514,7 @@ class BuilderTest(unittest.TestCase): '} name;' 'table STAT {' ' ElidedFallbackNameID 256;' + ' DesignAxis opsz 1 { name "Optical Size"; };' '} STAT;') def test_STAT_design_axis_name(self): @@ -647,7 +648,7 @@ class BuilderTest(unittest.TestCase): def test_STAT_invalid_location_tag(self): self.assertRaisesRegex( FeatureLibError, - 'Tags can not be longer than 4 characters', + 'Tags cannot be longer than 4 characters', self.build, 'table name {' ' nameid 256 "Roman"; ' diff --git a/Tests/feaLib/data/STAT_bad.fea b/Tests/feaLib/data/STAT_bad.fea index 8c87aabcc..8ec887f0e 100644 --- a/Tests/feaLib/data/STAT_bad.fea +++ b/Tests/feaLib/data/STAT_bad.fea @@ -1,3 +1,4 @@ +# bad fea file: Testing DesignAxis tag with incorrect label table name { nameid 25 "TestFont"; } name; @@ -7,7 +8,7 @@ table STAT { ElidedFallbackName { name "Roman"; }; - DesignAxis opsz 0 { badtag "Optical Size"; }; + DesignAxis opsz 0 { badtag "Optical Size"; }; #'badtag' instead of 'name' is incorrect DesignAxis wdth 1 { name "Width"; }; DesignAxis wght 2 { name "Weight"; }; DesignAxis ital 3 { name "Italic"; }; diff --git a/Tests/feaLib/data/STAT_test.ttx b/Tests/feaLib/data/STAT_test.ttx index caef1b0ad..d1b2b6970 100644 --- a/Tests/feaLib/data/STAT_test.ttx +++ b/Tests/feaLib/data/STAT_test.ttx @@ -1,5 +1,5 @@ - + @@ -15,59 +15,59 @@ Optical Size - Width - - - Weight - - - Italic - - - Caption - - Text - + Subhead - + Display - + + Width + + Condensed - + Semicondensed - + Normal - + Extended - + + Weight + + Light - + Regular - + Medium - + Semibold - + Bold - + Black - + + Italic + + Roman + + Caption + @@ -82,17 +82,17 @@ - + - + - + @@ -101,7 +101,7 @@ - + @@ -114,7 +114,7 @@ - + @@ -122,7 +122,7 @@ - + @@ -130,7 +130,7 @@ - + @@ -138,7 +138,7 @@ - + @@ -146,7 +146,7 @@ - + @@ -154,7 +154,7 @@ - + @@ -162,7 +162,7 @@ - + @@ -170,7 +170,7 @@ - + @@ -178,7 +178,7 @@ - + @@ -186,7 +186,7 @@ - + @@ -194,7 +194,7 @@ - + @@ -202,7 +202,7 @@ - + @@ -210,7 +210,7 @@ - + @@ -218,7 +218,7 @@ - + diff --git a/Tests/feaLib/data/STAT_test_elidedFallbackNameID.fea b/Tests/feaLib/data/STAT_test_elidedFallbackNameID.fea new file mode 100644 index 000000000..5a1418037 --- /dev/null +++ b/Tests/feaLib/data/STAT_test_elidedFallbackNameID.fea @@ -0,0 +1,84 @@ +table name { + nameid 25 "TestFont"; + nameid 256 "Roman"; +} name; +table STAT { + ElidedFallbackNameID 256; + DesignAxis opsz 0 { + name "Optical Size"; + }; + DesignAxis wdth 1 { + name "Width"; + }; + DesignAxis wght 2 { + name "Weight"; + }; + DesignAxis ital 3 { + name "Italic"; + }; # here comment + AxisValue { + location opsz 8; # comment here + location wdth 400; # another comment + name "Caption"; # more comments + }; + AxisValue { + location opsz 11 9 12; + name "Text"; + flag OlderSiblingFontAttribute ElidableAxisValueName; + }; + AxisValue { + location opsz 16.7 12 24; + name "Subhead"; + }; + AxisValue { + location opsz 72 24 72; + name "Display"; + }; + AxisValue { + location wdth 80 80 89; + name "Condensed"; + }; + AxisValue { + location wdth 90 90 96; + name "Semicondensed"; + }; + AxisValue { + location wdth 100 97 101; + name "Normal"; + flag ElidableAxisValueName; + }; + AxisValue { + location wdth 125 102 125; + name "Extended"; + }; + AxisValue { + location wght 300 300 349; + name "Light"; + }; + AxisValue { + location wght 400 350 449; + name "Regular"; + flag ElidableAxisValueName; + }; + AxisValue { + location wght 500 450 549; + name "Medium"; + }; + AxisValue { + location wght 600 550 649; + name "Semibold"; + }; + AxisValue { + location wght 700 650 749; + name "Bold"; + }; + AxisValue { + location wght 900 750 900; + name "Black"; + }; + AxisValue { + location ital 0; + name "Roman"; + flag ElidableAxisValueName; # flag comment + }; +} STAT; \ No newline at end of file diff --git a/Tests/feaLib/data/STAT_test_elidedFallbackNameID.ttx b/Tests/feaLib/data/STAT_test_elidedFallbackNameID.ttx new file mode 100644 index 000000000..32802e0fe --- /dev/null +++ b/Tests/feaLib/data/STAT_test_elidedFallbackNameID.ttx @@ -0,0 +1,225 @@ + + + + + + TestFont + + + Roman + + + Optical Size + + + Text + + + Subhead + + + Display + + + Width + + + Condensed + + + Semicondensed + + + Normal + + + Extended + + + Weight + + + Light + + + Regular + + + Medium + + + Semibold + + + Bold + + + Black + + + Italic + + + Roman + + + Caption + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py index 8b21a4ec5..de2bc3ca8 100644 --- a/Tests/feaLib/parser_test.py +++ b/Tests/feaLib/parser_test.py @@ -1284,7 +1284,7 @@ class ParserTest(unittest.TestCase): doc = self.parse('table STAT { DesignAxis opsz 0 ' '{name "Optical Size";}; } STAT;') da = doc.statements[0].statements[0] - self.assertIsInstance(da, ast.STATDesignAxis) + self.assertIsInstance(da, ast.STATDesignAxisStatement) self.assertEqual(da.tag, 'opsz') self.assertEqual(da.axisOrder, 0) self.assertEqual(da.names[0].string, 'Optical Size') @@ -1295,7 +1295,7 @@ class ParserTest(unittest.TestCase): 'AxisValue {location opsz 8; name "Caption";}; } ' 'STAT;') avr = doc.statements[0].statements[1] - self.assertIsInstance(avr, ast.STATAxisValueRecord) + self.assertIsInstance(avr, ast.STATAxisValueStatement) self.assertEqual(avr.locations[0].tag, 'opsz') self.assertEqual(avr.locations[0].values[0], 8) self.assertEqual(avr.names[0].string, 'Caption') @@ -1306,7 +1306,7 @@ class ParserTest(unittest.TestCase): 'AxisValue {location opsz 8 6 10; name "Caption";}; } ' 'STAT;') avr = doc.statements[0].statements[1] - self.assertIsInstance(avr, ast.STATAxisValueRecord) + self.assertIsInstance(avr, ast.STATAxisValueStatement) self.assertEqual(avr.locations[0].tag, 'opsz') self.assertEqual(avr.locations[0].values, [8, 6, 10]) self.assertEqual(avr.names[0].string, 'Caption')