[feaLib] Support stylistic set featureNames

Does not handle featureNames for cvXX features, but it shouldn’t be hard
for someone to extend the code to support them if inclined to do so.
This commit is contained in:
Khaled Hosny 2016-03-18 09:55:51 +04:00
parent 53dc98be55
commit 9feaab13aa
6 changed files with 158 additions and 16 deletions

View File

@ -591,3 +591,8 @@ class NameRecord(Statement):
builder.add_name_record( builder.add_name_record(
self.location, self.nameID, self.platformID, self.location, self.nameID, self.platformID,
self.platEncID, self.langID, self.string) self.platEncID, self.langID, self.string)
class FeatureNameStatement(NameRecord):
def build(self, builder):
NameRecord.build(self, builder)
builder.add_featureName(self.location, self.nameID)

View File

@ -35,6 +35,9 @@ class Builder(object):
self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
self.aalt_location_ = None self.aalt_location_ = None
self.aalt_alternates_ = {} self.aalt_alternates_ = {}
# for 'featureNames'
self.featureNames_ = []
self.featureNames_ids_ = {}
# for table 'head' # for table 'head'
self.fontRevision_ = None # 2.71 self.fontRevision_ = None # 2.71
# for table 'name' # for table 'name'
@ -157,14 +160,38 @@ class Builder(object):
table.created = table.modified = 3406620153 # 2011-12-13 11:22:33 table.created = table.modified = 3406620153 # 2011-12-13 11:22:33
table.fontRevision = self.fontRevision_ table.fontRevision = self.fontRevision_
def get_user_name_id(self, table):
# Try to find first unused font-specific name id
nameIDs = [name.nameID for name in table.names]
for user_name_id in range(256, 32767):
if user_name_id not in nameIDs:
return user_name_id
def buildFeatureParams(self, tag):
params = None
if tag in self.featureNames_:
assert tag in self.featureNames_ids_
params = otTables.FeatureParamsStylisticSet()
params.Version = 0
params.UINameID = self.featureNames_ids_[tag]
return params
def build_name(self): def build_name(self):
if not self.names_: if not self.names_:
return return
table = self.font.get("name") table = self.font.get("name")
if not table: # this only happens for unit tests if not table: # this only happens for unit tests
table = self.font["name"] = getTableClass("name")() table = self.font["name"] = getTableClass("name")()
table.names = []
for name in self.names_: for name in self.names_:
nameID, platformID, platEncID, langID, string = name nameID, platformID, platEncID, langID, string = name
if not isinstance(nameID, int):
# A featureNames name and nameID is actually the tag
tag = nameID
if tag not in self.featureNames_ids_:
self.featureNames_ids_[tag] = self.get_user_name_id(table)
assert self.featureNames_ids_[tag] is not None
nameID = self.featureNames_ids_[tag]
table.setName(string, nameID, platformID, platEncID, langID) table.setName(string, nameID, platformID, platEncID, langID)
def buildGDEF(self): def buildGDEF(self):
@ -276,7 +303,8 @@ class Builder(object):
frec = otTables.FeatureRecord() frec = otTables.FeatureRecord()
frec.FeatureTag = feature_tag frec.FeatureTag = feature_tag
frec.Feature = otTables.Feature() frec.Feature = otTables.Feature()
frec.Feature.FeatureParams = None frec.Feature.FeatureParams = self.buildFeatureParams(
feature_tag)
frec.Feature.LookupListIndex = lookup_indices frec.Feature.LookupListIndex = lookup_indices
frec.Feature.LookupCount = len(lookup_indices) frec.Feature.LookupCount = len(lookup_indices)
table.FeatureList.FeatureRecord.append(frec) table.FeatureList.FeatureRecord.append(frec)
@ -523,6 +551,9 @@ class Builder(object):
location) location)
self.aalt_features_.append((location, featureName)) self.aalt_features_.append((location, featureName))
def add_featureName(self, location, tag):
self.featureNames_.append(tag)
def add_ligature_subst(self, location, def add_ligature_subst(self, location,
prefix, glyphs, suffix, replacement, forceChain): prefix, glyphs, suffix, replacement, forceChain):
if prefix or suffix or forceChain: if prefix or suffix or forceChain:

View File

@ -56,7 +56,7 @@ class BuilderTest(unittest.TestCase):
spec4h1 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4 spec4h1 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4
spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4 spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4
spec5h1 spec6b_ii spec6d2 spec6e spec6f spec5h1 spec6b_ii spec6d2 spec6e spec6f
spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8c
spec9b spec9c1 spec9c2 spec9c3 spec9e spec9b spec9c1 spec9c2 spec9c3 spec9e
bug453 bug463 bug501 bug502 bug505 bug506 bug509 bug512 bug453 bug463 bug501 bug502 bug505 bug506 bug509 bug512
name name

View File

@ -726,16 +726,7 @@ class Parser(object):
raise FeatureLibError("Expected nameid", raise FeatureLibError("Expected nameid",
self.cur_token_location_) self.cur_token_location_)
def parse_nameid_(self): def parse_name_(self):
assert self.cur_token_ == "nameid", self.cur_token_
location, nameID = self.cur_token_location_, self.expect_number_()
if nameID > 255:
raise FeatureLibError("Expected name id < 255",
self.cur_token_location_)
if 1 <= nameID <= 6:
# TODO: issue a warning
return None
platEncID = None platEncID = None
langID = None langID = None
if self.next_token_type_ == Lexer.NUMBER: if self.next_token_type_ == Lexer.NUMBER:
@ -764,8 +755,21 @@ class Parser(object):
elif platformID == 3 and platEncID == 1: elif platformID == 3 and platEncID == 1:
string = self.unescape_windows_name_string(string) string = self.unescape_windows_name_string(string)
return ast.NameRecord(location, nameID, platformID, return platformID, platEncID, langID, string
platEncID, langID, string)
def parse_nameid_(self):
assert self.cur_token_ == "nameid", self.cur_token_
location, nameID = self.cur_token_location_, self.expect_number_()
if nameID > 255:
raise FeatureLibError("Expected name id < 255",
self.cur_token_location_)
if 1 <= nameID <= 6:
# TODO: issue a warning
return None
platformID, platEncID, langID, string = self.parse_name_()
return ast.NameRecord(location, nameID, platformID, platEncID,
langID, string)
def unescape_mac_name_string(self, string): def unescape_mac_name_string(self, string):
def unescape(match): def unescape(match):
@ -881,6 +885,9 @@ class Parser(object):
location = self.cur_token_location_ location = self.cur_token_location_
tag = self.expect_tag_() tag = self.expect_tag_()
vertical = (tag in {"vkrn", "vpal", "vhal", "valt"}) vertical = (tag in {"vkrn", "vpal", "vhal", "valt"})
stylisticset = None
if tag in ["ss%02d" % i for i in range(1, 20+1)]:
stylisticset = tag
use_extension = False use_extension = False
if self.next_token_ == "useExtension": if self.next_token_ == "useExtension":
@ -888,7 +895,7 @@ class Parser(object):
use_extension = True use_extension = True
block = ast.FeatureBlock(location, tag, use_extension) block = ast.FeatureBlock(location, tag, use_extension)
self.parse_block_(block, vertical) self.parse_block_(block, vertical, stylisticset)
return block return block
def parse_feature_reference_(self): def parse_feature_reference_(self):
@ -898,6 +905,30 @@ class Parser(object):
self.expect_symbol_(";") self.expect_symbol_(";")
return ast.FeatureReferenceStatement(location, featureName) return ast.FeatureReferenceStatement(location, featureName)
def parse_featureNames_(self, tag):
assert self.cur_token_ == "featureNames", self.cur_token_
self.expect_symbol_("{")
for symtab in self.symbol_tables_:
symtab.enter_scope()
statements = []
while self.next_token_ != "}":
self.expect_keyword_("name")
location = self.cur_token_location_
platformID, platEncID, langID, string = self.parse_name_()
statements.append(
ast.FeatureNameStatement(location, tag, platformID,
platEncID, langID, string))
self.expect_symbol_("}")
for symtab in self.symbol_tables_:
symtab.exit_scope()
self.expect_symbol_(";")
return statements
def parse_FontRevision_(self): def parse_FontRevision_(self):
assert self.cur_token_ == "FontRevision", self.cur_token_ assert self.cur_token_ == "FontRevision", self.cur_token_
location, version = self.cur_token_location_, self.expect_float_() location, version = self.cur_token_location_, self.expect_float_()
@ -907,7 +938,7 @@ class Parser(object):
location) location)
return ast.FontRevisionStatement(location, version) return ast.FontRevisionStatement(location, version)
def parse_block_(self, block, vertical): def parse_block_(self, block, vertical, stylisticset=None):
self.expect_symbol_("{") self.expect_symbol_("{")
for symtab in self.symbol_tables_: for symtab in self.symbol_tables_:
symtab.enter_scope() symtab.enter_scope()
@ -945,6 +976,8 @@ class Parser(object):
statements.append(self.parse_subtable_()) statements.append(self.parse_subtable_())
elif self.is_cur_keyword_("valueRecordDef"): elif self.is_cur_keyword_("valueRecordDef"):
statements.append(self.parse_valuerecord_definition_(vertical)) statements.append(self.parse_valuerecord_definition_(vertical))
elif stylisticset and self.is_cur_keyword_("featureNames"):
statements.extend(self.parse_featureNames_(stylisticset))
elif self.cur_token_ == ";": elif self.cur_token_ == ";":
continue continue
else: else:

View File

@ -0,0 +1,11 @@
feature ss01 {
featureNames {
name "Feature description for MS Platform, script Unicode, language English";
# With no platform ID, script ID, or language ID specified, the implementation assumes (3,1,0x409).
name 3 1 0x411 "Feature description for MS Platform, script Unicode, language Japanese";
name 1 "Feature description for Apple Platform, script Roman, language unspecified";
# With only the platform ID specified, the implementation assumes script and language = Latin. For Apple this is (1,0,0).
name 1 1 12 "Feature description for Apple Platform, script Japanese, language Japanese"; } ;
# --- rules for this feature ---
sub A by B;
} ss01;

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<name>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Feature description for MS Platform, script Unicode, language English
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x411">
Feature description for MS Platform, script Unicode, language Japanese
</namerecord>
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
Feature description for Apple Platform, script Roman, language unspecified
</namerecord>
<namerecord nameID="256" platformID="1" platEncID="1" langID="0xc" unicode="True">
Feature description for Apple Platform, script Japanese, language Japanese
</namerecord>
</name>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="ss01"/>
<Feature>
<FeatureParamsStylisticSet>
<Version value="0"/>
<UINameID value="256"/>
</FeatureParamsStylisticSet>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<!-- LookupType=1 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
<Substitution in="A" out="B"/>
</SingleSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>