1*e1fe3e4aSElliott Hughesfrom fontTools.misc.loggingTools import CapturingLogHandler 2*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.builder import ( 3*e1fe3e4aSElliott Hughes Builder, 4*e1fe3e4aSElliott Hughes addOpenTypeFeatures, 5*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString, 6*e1fe3e4aSElliott Hughes) 7*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.error import FeatureLibError 8*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont, newTable 9*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.parser import Parser 10*e1fe3e4aSElliott Hughesfrom fontTools.feaLib import ast 11*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.lexer import Lexer 12*e1fe3e4aSElliott Hughesfrom fontTools.fontBuilder import addFvar 13*e1fe3e4aSElliott Hughesimport difflib 14*e1fe3e4aSElliott Hughesfrom io import StringIO 15*e1fe3e4aSElliott Hughesimport os 16*e1fe3e4aSElliott Hughesimport re 17*e1fe3e4aSElliott Hughesimport shutil 18*e1fe3e4aSElliott Hughesimport sys 19*e1fe3e4aSElliott Hughesimport tempfile 20*e1fe3e4aSElliott Hughesimport logging 21*e1fe3e4aSElliott Hughesimport unittest 22*e1fe3e4aSElliott Hughesimport warnings 23*e1fe3e4aSElliott Hughes 24*e1fe3e4aSElliott Hughes 25*e1fe3e4aSElliott Hughesdef makeTTFont(): 26*e1fe3e4aSElliott Hughes glyphs = """ 27*e1fe3e4aSElliott Hughes .notdef space slash fraction semicolon period comma ampersand 28*e1fe3e4aSElliott Hughes quotedblleft quotedblright quoteleft quoteright 29*e1fe3e4aSElliott Hughes zero one two three four five six seven eight nine 30*e1fe3e4aSElliott Hughes zero.oldstyle one.oldstyle two.oldstyle three.oldstyle 31*e1fe3e4aSElliott Hughes four.oldstyle five.oldstyle six.oldstyle seven.oldstyle 32*e1fe3e4aSElliott Hughes eight.oldstyle nine.oldstyle onequarter onehalf threequarters 33*e1fe3e4aSElliott Hughes onesuperior twosuperior threesuperior ordfeminine ordmasculine 34*e1fe3e4aSElliott Hughes A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 35*e1fe3e4aSElliott Hughes a b c d e f g h i j k l m n o p q r s t u v w x y z 36*e1fe3e4aSElliott Hughes A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc 37*e1fe3e4aSElliott Hughes N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc 38*e1fe3e4aSElliott Hughes A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3 39*e1fe3e4aSElliott Hughes a.alt1 a.alt2 a.alt3 a.end b.alt c.mid d.alt d.mid 40*e1fe3e4aSElliott Hughes e.begin e.mid e.end m.begin n.end s.end z.end 41*e1fe3e4aSElliott Hughes Eng Eng.alt1 Eng.alt2 Eng.alt3 42*e1fe3e4aSElliott Hughes A.swash B.swash C.swash D.swash E.swash F.swash G.swash H.swash 43*e1fe3e4aSElliott Hughes I.swash J.swash K.swash L.swash M.swash N.swash O.swash P.swash 44*e1fe3e4aSElliott Hughes Q.swash R.swash S.swash T.swash U.swash V.swash W.swash X.swash 45*e1fe3e4aSElliott Hughes Y.swash Z.swash 46*e1fe3e4aSElliott Hughes f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t f_i.begin 47*e1fe3e4aSElliott Hughes a_n_d T_h T_h.swash germandbls ydieresis yacute breve 48*e1fe3e4aSElliott Hughes grave acute dieresis macron circumflex cedilla umlaut ogonek caron 49*e1fe3e4aSElliott Hughes damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial 50*e1fe3e4aSElliott Hughes by feature lookup sub table uni0327 uni0328 e.fina 51*e1fe3e4aSElliott Hughes """.split() 52*e1fe3e4aSElliott Hughes glyphs.extend("cid{:05d}".format(cid) for cid in range(800, 1001 + 1)) 53*e1fe3e4aSElliott Hughes font = TTFont() 54*e1fe3e4aSElliott Hughes font.setGlyphOrder(glyphs) 55*e1fe3e4aSElliott Hughes return font 56*e1fe3e4aSElliott Hughes 57*e1fe3e4aSElliott Hughes 58*e1fe3e4aSElliott Hughesclass BuilderTest(unittest.TestCase): 59*e1fe3e4aSElliott Hughes # Feature files in data/*.fea; output gets compared to data/*.ttx. 60*e1fe3e4aSElliott Hughes TEST_FEATURE_FILES = """ 61*e1fe3e4aSElliott Hughes Attach cid_range enum markClass language_required 62*e1fe3e4aSElliott Hughes GlyphClassDef LigatureCaretByIndex LigatureCaretByPos 63*e1fe3e4aSElliott Hughes lookup lookupflag feature_aalt ignore_pos 64*e1fe3e4aSElliott Hughes GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8 65*e1fe3e4aSElliott Hughes GSUB_2 GSUB_3 GSUB_6 GSUB_8 66*e1fe3e4aSElliott Hughes spec4h1 spec4h2 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4 67*e1fe3e4aSElliott Hughes spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4 68*e1fe3e4aSElliott Hughes spec5h1 spec6b_ii spec6d2 spec6e spec6f 69*e1fe3e4aSElliott Hughes spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c spec8d 70*e1fe3e4aSElliott Hughes spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g 71*e1fe3e4aSElliott Hughes spec10 72*e1fe3e4aSElliott Hughes bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509 73*e1fe3e4aSElliott Hughes bug512 bug514 bug568 bug633 bug1307 bug1459 bug2276 variable_bug2772 74*e1fe3e4aSElliott Hughes name size size2 multiple_feature_blocks omitted_GlyphClassDef 75*e1fe3e4aSElliott Hughes ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical 76*e1fe3e4aSElliott Hughes ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical 77*e1fe3e4aSElliott Hughes ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical 78*e1fe3e4aSElliott Hughes PairPosSubtable ChainSubstSubtable SubstSubtable ChainPosSubtable 79*e1fe3e4aSElliott Hughes LigatureSubtable AlternateSubtable MultipleSubstSubtable 80*e1fe3e4aSElliott Hughes SingleSubstSubtable aalt_chain_contextual_subst AlternateChained 81*e1fe3e4aSElliott Hughes MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats 82*e1fe3e4aSElliott Hughes GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID 83*e1fe3e4aSElliott Hughes variable_scalar_valuerecord variable_scalar_anchor variable_conditionset 84*e1fe3e4aSElliott Hughes variable_mark_anchor 85*e1fe3e4aSElliott Hughes """.split() 86*e1fe3e4aSElliott Hughes 87*e1fe3e4aSElliott Hughes VARFONT_AXES = [ 88*e1fe3e4aSElliott Hughes ("wght", 200, 200, 1000, "Weight"), 89*e1fe3e4aSElliott Hughes ("wdth", 100, 100, 200, "Width"), 90*e1fe3e4aSElliott Hughes ] 91*e1fe3e4aSElliott Hughes 92*e1fe3e4aSElliott Hughes def __init__(self, methodName): 93*e1fe3e4aSElliott Hughes unittest.TestCase.__init__(self, methodName) 94*e1fe3e4aSElliott Hughes # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 95*e1fe3e4aSElliott Hughes # and fires deprecation warnings if a program uses the old name. 96*e1fe3e4aSElliott Hughes if not hasattr(self, "assertRaisesRegex"): 97*e1fe3e4aSElliott Hughes self.assertRaisesRegex = self.assertRaisesRegexp 98*e1fe3e4aSElliott Hughes 99*e1fe3e4aSElliott Hughes def setUp(self): 100*e1fe3e4aSElliott Hughes self.tempdir = None 101*e1fe3e4aSElliott Hughes self.num_tempfiles = 0 102*e1fe3e4aSElliott Hughes 103*e1fe3e4aSElliott Hughes def tearDown(self): 104*e1fe3e4aSElliott Hughes if self.tempdir: 105*e1fe3e4aSElliott Hughes shutil.rmtree(self.tempdir) 106*e1fe3e4aSElliott Hughes 107*e1fe3e4aSElliott Hughes @staticmethod 108*e1fe3e4aSElliott Hughes def getpath(testfile): 109*e1fe3e4aSElliott Hughes path, _ = os.path.split(__file__) 110*e1fe3e4aSElliott Hughes return os.path.join(path, "data", testfile) 111*e1fe3e4aSElliott Hughes 112*e1fe3e4aSElliott Hughes def temp_path(self, suffix): 113*e1fe3e4aSElliott Hughes if not self.tempdir: 114*e1fe3e4aSElliott Hughes self.tempdir = tempfile.mkdtemp() 115*e1fe3e4aSElliott Hughes self.num_tempfiles += 1 116*e1fe3e4aSElliott Hughes return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix)) 117*e1fe3e4aSElliott Hughes 118*e1fe3e4aSElliott Hughes def read_ttx(self, path): 119*e1fe3e4aSElliott Hughes lines = [] 120*e1fe3e4aSElliott Hughes with open(path, "r", encoding="utf-8") as ttx: 121*e1fe3e4aSElliott Hughes for line in ttx.readlines(): 122*e1fe3e4aSElliott Hughes # Elide ttFont attributes because ttLibVersion may change. 123*e1fe3e4aSElliott Hughes if line.startswith("<ttFont "): 124*e1fe3e4aSElliott Hughes lines.append("<ttFont>\n") 125*e1fe3e4aSElliott Hughes else: 126*e1fe3e4aSElliott Hughes lines.append(line.rstrip() + "\n") 127*e1fe3e4aSElliott Hughes return lines 128*e1fe3e4aSElliott Hughes 129*e1fe3e4aSElliott Hughes def expect_ttx(self, font, expected_ttx, replace=None): 130*e1fe3e4aSElliott Hughes path = self.temp_path(suffix=".ttx") 131*e1fe3e4aSElliott Hughes font.saveXML( 132*e1fe3e4aSElliott Hughes path, 133*e1fe3e4aSElliott Hughes tables=[ 134*e1fe3e4aSElliott Hughes "head", 135*e1fe3e4aSElliott Hughes "name", 136*e1fe3e4aSElliott Hughes "BASE", 137*e1fe3e4aSElliott Hughes "GDEF", 138*e1fe3e4aSElliott Hughes "GSUB", 139*e1fe3e4aSElliott Hughes "GPOS", 140*e1fe3e4aSElliott Hughes "OS/2", 141*e1fe3e4aSElliott Hughes "STAT", 142*e1fe3e4aSElliott Hughes "hhea", 143*e1fe3e4aSElliott Hughes "vhea", 144*e1fe3e4aSElliott Hughes ], 145*e1fe3e4aSElliott Hughes ) 146*e1fe3e4aSElliott Hughes actual = self.read_ttx(path) 147*e1fe3e4aSElliott Hughes expected = self.read_ttx(expected_ttx) 148*e1fe3e4aSElliott Hughes if replace: 149*e1fe3e4aSElliott Hughes for i in range(len(expected)): 150*e1fe3e4aSElliott Hughes for k, v in replace.items(): 151*e1fe3e4aSElliott Hughes expected[i] = expected[i].replace(k, v) 152*e1fe3e4aSElliott Hughes if actual != expected: 153*e1fe3e4aSElliott Hughes for line in difflib.unified_diff( 154*e1fe3e4aSElliott Hughes expected, actual, fromfile=expected_ttx, tofile=path 155*e1fe3e4aSElliott Hughes ): 156*e1fe3e4aSElliott Hughes sys.stderr.write(line) 157*e1fe3e4aSElliott Hughes self.fail("TTX output is different from expected") 158*e1fe3e4aSElliott Hughes 159*e1fe3e4aSElliott Hughes def build(self, featureFile, tables=None): 160*e1fe3e4aSElliott Hughes font = makeTTFont() 161*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString(font, featureFile, tables=tables) 162*e1fe3e4aSElliott Hughes return font 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughes def check_feature_file(self, name): 165*e1fe3e4aSElliott Hughes font = makeTTFont() 166*e1fe3e4aSElliott Hughes if name.startswith("variable_"): 167*e1fe3e4aSElliott Hughes font["name"] = newTable("name") 168*e1fe3e4aSElliott Hughes addFvar(font, self.VARFONT_AXES, []) 169*e1fe3e4aSElliott Hughes del font["name"] 170*e1fe3e4aSElliott Hughes feapath = self.getpath("%s.fea" % name) 171*e1fe3e4aSElliott Hughes addOpenTypeFeatures(font, feapath) 172*e1fe3e4aSElliott Hughes self.expect_ttx(font, self.getpath("%s.ttx" % name)) 173*e1fe3e4aSElliott Hughes # Check that: 174*e1fe3e4aSElliott Hughes # 1) tables do compile (only G* tables as long as we have a mock font) 175*e1fe3e4aSElliott Hughes # 2) dumping after save-reload yields the same TTX dump as before 176*e1fe3e4aSElliott Hughes for tag in ("GDEF", "GSUB", "GPOS"): 177*e1fe3e4aSElliott Hughes if tag in font: 178*e1fe3e4aSElliott Hughes data = font[tag].compile(font) 179*e1fe3e4aSElliott Hughes font[tag].decompile(data, font) 180*e1fe3e4aSElliott Hughes self.expect_ttx(font, self.getpath("%s.ttx" % name)) 181*e1fe3e4aSElliott Hughes # Optionally check a debug dump. 182*e1fe3e4aSElliott Hughes debugttx = self.getpath("%s-debug.ttx" % name) 183*e1fe3e4aSElliott Hughes if os.path.exists(debugttx): 184*e1fe3e4aSElliott Hughes addOpenTypeFeatures(font, feapath, debug=True) 185*e1fe3e4aSElliott Hughes self.expect_ttx(font, debugttx, replace={"__PATH__": feapath}) 186*e1fe3e4aSElliott Hughes 187*e1fe3e4aSElliott Hughes def check_fea2fea_file(self, name, base=None, parser=Parser): 188*e1fe3e4aSElliott Hughes font = makeTTFont() 189*e1fe3e4aSElliott Hughes fname = (name + ".fea") if "." not in name else name 190*e1fe3e4aSElliott Hughes p = parser(self.getpath(fname), glyphNames=font.getGlyphOrder()) 191*e1fe3e4aSElliott Hughes doc = p.parse() 192*e1fe3e4aSElliott Hughes actual = self.normal_fea(doc.asFea().split("\n")) 193*e1fe3e4aSElliott Hughes with open(self.getpath(base or fname), "r", encoding="utf-8") as ofile: 194*e1fe3e4aSElliott Hughes expected = self.normal_fea(ofile.readlines()) 195*e1fe3e4aSElliott Hughes 196*e1fe3e4aSElliott Hughes if expected != actual: 197*e1fe3e4aSElliott Hughes fname = name.rsplit(".", 1)[0] + ".fea" 198*e1fe3e4aSElliott Hughes for line in difflib.unified_diff( 199*e1fe3e4aSElliott Hughes expected, 200*e1fe3e4aSElliott Hughes actual, 201*e1fe3e4aSElliott Hughes fromfile=fname + " (expected)", 202*e1fe3e4aSElliott Hughes tofile=fname + " (actual)", 203*e1fe3e4aSElliott Hughes ): 204*e1fe3e4aSElliott Hughes sys.stderr.write(line + "\n") 205*e1fe3e4aSElliott Hughes self.fail( 206*e1fe3e4aSElliott Hughes "Fea2Fea output is different from expected. " 207*e1fe3e4aSElliott Hughes "Generated:\n{}\n".format("\n".join(actual)) 208*e1fe3e4aSElliott Hughes ) 209*e1fe3e4aSElliott Hughes 210*e1fe3e4aSElliott Hughes def normal_fea(self, lines): 211*e1fe3e4aSElliott Hughes output = [] 212*e1fe3e4aSElliott Hughes skip = 0 213*e1fe3e4aSElliott Hughes for l in lines: 214*e1fe3e4aSElliott Hughes l = l.strip() 215*e1fe3e4aSElliott Hughes if l.startswith("#test-fea2fea:"): 216*e1fe3e4aSElliott Hughes if len(l) > 15: 217*e1fe3e4aSElliott Hughes output.append(l[15:].strip()) 218*e1fe3e4aSElliott Hughes skip = 1 219*e1fe3e4aSElliott Hughes x = l.find("#") 220*e1fe3e4aSElliott Hughes if x >= 0: 221*e1fe3e4aSElliott Hughes l = l[:x].strip() 222*e1fe3e4aSElliott Hughes if not len(l): 223*e1fe3e4aSElliott Hughes continue 224*e1fe3e4aSElliott Hughes if skip > 0: 225*e1fe3e4aSElliott Hughes skip = skip - 1 226*e1fe3e4aSElliott Hughes continue 227*e1fe3e4aSElliott Hughes output.append(l) 228*e1fe3e4aSElliott Hughes return output 229*e1fe3e4aSElliott Hughes 230*e1fe3e4aSElliott Hughes def make_mock_vf(self): 231*e1fe3e4aSElliott Hughes font = makeTTFont() 232*e1fe3e4aSElliott Hughes font["name"] = newTable("name") 233*e1fe3e4aSElliott Hughes addFvar(font, self.VARFONT_AXES, []) 234*e1fe3e4aSElliott Hughes del font["name"] 235*e1fe3e4aSElliott Hughes return font 236*e1fe3e4aSElliott Hughes 237*e1fe3e4aSElliott Hughes @staticmethod 238*e1fe3e4aSElliott Hughes def get_region(var_region_axis): 239*e1fe3e4aSElliott Hughes return ( 240*e1fe3e4aSElliott Hughes var_region_axis.StartCoord, 241*e1fe3e4aSElliott Hughes var_region_axis.PeakCoord, 242*e1fe3e4aSElliott Hughes var_region_axis.EndCoord, 243*e1fe3e4aSElliott Hughes ) 244*e1fe3e4aSElliott Hughes 245*e1fe3e4aSElliott Hughes def test_alternateSubst_multipleSubstitutionsForSameGlyph(self): 246*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 247*e1fe3e4aSElliott Hughes FeatureLibError, 248*e1fe3e4aSElliott Hughes 'Already defined alternates for glyph "A"', 249*e1fe3e4aSElliott Hughes self.build, 250*e1fe3e4aSElliott Hughes "feature test {" 251*e1fe3e4aSElliott Hughes " sub A from [A.alt1 A.alt2];" 252*e1fe3e4aSElliott Hughes " sub B from [B.alt1 B.alt2 B.alt3];" 253*e1fe3e4aSElliott Hughes " sub A from [A.alt1 A.alt2];" 254*e1fe3e4aSElliott Hughes "} test;", 255*e1fe3e4aSElliott Hughes ) 256*e1fe3e4aSElliott Hughes 257*e1fe3e4aSElliott Hughes def test_singleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self): 258*e1fe3e4aSElliott Hughes logger = logging.getLogger("fontTools.feaLib.builder") 259*e1fe3e4aSElliott Hughes with CapturingLogHandler(logger, "INFO") as captor: 260*e1fe3e4aSElliott Hughes self.build( 261*e1fe3e4aSElliott Hughes "feature test {" 262*e1fe3e4aSElliott Hughes " sub A by A.sc;" 263*e1fe3e4aSElliott Hughes " sub B by B.sc;" 264*e1fe3e4aSElliott Hughes " sub A by A.sc;" 265*e1fe3e4aSElliott Hughes "} test;" 266*e1fe3e4aSElliott Hughes ) 267*e1fe3e4aSElliott Hughes captor.assertRegex( 268*e1fe3e4aSElliott Hughes 'Removing duplicate single substitution from glyph "A" to "A.sc"' 269*e1fe3e4aSElliott Hughes ) 270*e1fe3e4aSElliott Hughes 271*e1fe3e4aSElliott Hughes def test_multipleSubst_multipleSubstitutionsForSameGlyph(self): 272*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 273*e1fe3e4aSElliott Hughes FeatureLibError, 274*e1fe3e4aSElliott Hughes 'Already defined substitution for glyph "f_f_i"', 275*e1fe3e4aSElliott Hughes self.build, 276*e1fe3e4aSElliott Hughes "feature test {" 277*e1fe3e4aSElliott Hughes " sub f_f_i by f f i;" 278*e1fe3e4aSElliott Hughes " sub c_t by c t;" 279*e1fe3e4aSElliott Hughes " sub f_f_i by f_f i;" 280*e1fe3e4aSElliott Hughes "} test;", 281*e1fe3e4aSElliott Hughes ) 282*e1fe3e4aSElliott Hughes 283*e1fe3e4aSElliott Hughes def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self): 284*e1fe3e4aSElliott Hughes logger = logging.getLogger("fontTools.feaLib.builder") 285*e1fe3e4aSElliott Hughes with CapturingLogHandler(logger, "INFO") as captor: 286*e1fe3e4aSElliott Hughes self.build( 287*e1fe3e4aSElliott Hughes "feature test {" 288*e1fe3e4aSElliott Hughes " sub f_f_i by f f i;" 289*e1fe3e4aSElliott Hughes " sub c_t by c t;" 290*e1fe3e4aSElliott Hughes " sub f_f_i by f f i;" 291*e1fe3e4aSElliott Hughes "} test;" 292*e1fe3e4aSElliott Hughes ) 293*e1fe3e4aSElliott Hughes captor.assertRegex( 294*e1fe3e4aSElliott Hughes r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)" 295*e1fe3e4aSElliott Hughes ) 296*e1fe3e4aSElliott Hughes 297*e1fe3e4aSElliott Hughes def test_pairPos_redefinition_warning(self): 298*e1fe3e4aSElliott Hughes # https://github.com/fonttools/fonttools/issues/1147 299*e1fe3e4aSElliott Hughes logger = logging.getLogger("fontTools.otlLib.builder") 300*e1fe3e4aSElliott Hughes with CapturingLogHandler(logger, "DEBUG") as captor: 301*e1fe3e4aSElliott Hughes # the pair "yacute semicolon" is redefined in the enum pos 302*e1fe3e4aSElliott Hughes font = self.build( 303*e1fe3e4aSElliott Hughes "@Y_LC = [y yacute ydieresis];" 304*e1fe3e4aSElliott Hughes "@SMALL_PUNC = [comma semicolon period];" 305*e1fe3e4aSElliott Hughes "feature kern {" 306*e1fe3e4aSElliott Hughes " pos yacute semicolon -70;" 307*e1fe3e4aSElliott Hughes " enum pos @Y_LC semicolon -80;" 308*e1fe3e4aSElliott Hughes " pos @Y_LC @SMALL_PUNC -100;" 309*e1fe3e4aSElliott Hughes "} kern;" 310*e1fe3e4aSElliott Hughes ) 311*e1fe3e4aSElliott Hughes 312*e1fe3e4aSElliott Hughes captor.assertRegex("Already defined position for pair yacute semicolon") 313*e1fe3e4aSElliott Hughes 314*e1fe3e4aSElliott Hughes # the first definition prevails: yacute semicolon -70 315*e1fe3e4aSElliott Hughes st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0] 316*e1fe3e4aSElliott Hughes self.assertEqual(st.Coverage.glyphs[2], "yacute") 317*e1fe3e4aSElliott Hughes self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, "semicolon") 318*e1fe3e4aSElliott Hughes self.assertEqual( 319*e1fe3e4aSElliott Hughes vars(st.PairSet[2].PairValueRecord[0].Value1), {"XAdvance": -70} 320*e1fe3e4aSElliott Hughes ) 321*e1fe3e4aSElliott Hughes 322*e1fe3e4aSElliott Hughes def test_singleSubst_multipleSubstitutionsForSameGlyph(self): 323*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 324*e1fe3e4aSElliott Hughes FeatureLibError, 325*e1fe3e4aSElliott Hughes 'Already defined rule for replacing glyph "e" by "E.sc"', 326*e1fe3e4aSElliott Hughes self.build, 327*e1fe3e4aSElliott Hughes "feature test {" 328*e1fe3e4aSElliott Hughes " sub [a-z] by [A.sc-Z.sc];" 329*e1fe3e4aSElliott Hughes " sub e by e.fina;" 330*e1fe3e4aSElliott Hughes "} test;", 331*e1fe3e4aSElliott Hughes ) 332*e1fe3e4aSElliott Hughes 333*e1fe3e4aSElliott Hughes def test_singlePos_redefinition(self): 334*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 335*e1fe3e4aSElliott Hughes FeatureLibError, 336*e1fe3e4aSElliott Hughes 'Already defined different position for glyph "A"', 337*e1fe3e4aSElliott Hughes self.build, 338*e1fe3e4aSElliott Hughes "feature test { pos A 123; pos A 456; } test;", 339*e1fe3e4aSElliott Hughes ) 340*e1fe3e4aSElliott Hughes 341*e1fe3e4aSElliott Hughes def test_feature_outside_aalt(self): 342*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 343*e1fe3e4aSElliott Hughes FeatureLibError, 344*e1fe3e4aSElliott Hughes 'Feature references are only allowed inside "feature aalt"', 345*e1fe3e4aSElliott Hughes self.build, 346*e1fe3e4aSElliott Hughes "feature test { feature test; } test;", 347*e1fe3e4aSElliott Hughes ) 348*e1fe3e4aSElliott Hughes 349*e1fe3e4aSElliott Hughes def test_feature_undefinedReference(self): 350*e1fe3e4aSElliott Hughes with warnings.catch_warnings(record=True) as w: 351*e1fe3e4aSElliott Hughes self.build("feature aalt { feature none; } aalt;") 352*e1fe3e4aSElliott Hughes assert len(w) == 1 353*e1fe3e4aSElliott Hughes assert "Feature none has not been defined" in str(w[0].message) 354*e1fe3e4aSElliott Hughes 355*e1fe3e4aSElliott Hughes def test_GlyphClassDef_conflictingClasses(self): 356*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 357*e1fe3e4aSElliott Hughes FeatureLibError, 358*e1fe3e4aSElliott Hughes "Glyph X was assigned to a different class", 359*e1fe3e4aSElliott Hughes self.build, 360*e1fe3e4aSElliott Hughes "table GDEF {" 361*e1fe3e4aSElliott Hughes " GlyphClassDef [a b], [X], , ;" 362*e1fe3e4aSElliott Hughes " GlyphClassDef [a b X], , , ;" 363*e1fe3e4aSElliott Hughes "} GDEF;", 364*e1fe3e4aSElliott Hughes ) 365*e1fe3e4aSElliott Hughes 366*e1fe3e4aSElliott Hughes def test_languagesystem(self): 367*e1fe3e4aSElliott Hughes builder = Builder(makeTTFont(), (None, None)) 368*e1fe3e4aSElliott Hughes builder.add_language_system(None, "latn", "FRA") 369*e1fe3e4aSElliott Hughes builder.add_language_system(None, "cyrl", "RUS") 370*e1fe3e4aSElliott Hughes builder.start_feature(location=None, name="test") 371*e1fe3e4aSElliott Hughes self.assertEqual(builder.language_systems, {("latn", "FRA"), ("cyrl", "RUS")}) 372*e1fe3e4aSElliott Hughes 373*e1fe3e4aSElliott Hughes def test_languagesystem_duplicate(self): 374*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 375*e1fe3e4aSElliott Hughes FeatureLibError, 376*e1fe3e4aSElliott Hughes '"languagesystem cyrl RUS" has already been specified', 377*e1fe3e4aSElliott Hughes self.build, 378*e1fe3e4aSElliott Hughes "languagesystem cyrl RUS; languagesystem cyrl RUS;", 379*e1fe3e4aSElliott Hughes ) 380*e1fe3e4aSElliott Hughes 381*e1fe3e4aSElliott Hughes def test_languagesystem_none_specified(self): 382*e1fe3e4aSElliott Hughes builder = Builder(makeTTFont(), (None, None)) 383*e1fe3e4aSElliott Hughes builder.start_feature(location=None, name="test") 384*e1fe3e4aSElliott Hughes self.assertEqual(builder.language_systems, {("DFLT", "dflt")}) 385*e1fe3e4aSElliott Hughes 386*e1fe3e4aSElliott Hughes def test_languagesystem_DFLT_dflt_not_first(self): 387*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 388*e1fe3e4aSElliott Hughes FeatureLibError, 389*e1fe3e4aSElliott Hughes 'If "languagesystem DFLT dflt" is present, ' 390*e1fe3e4aSElliott Hughes "it must be the first of the languagesystem statements", 391*e1fe3e4aSElliott Hughes self.build, 392*e1fe3e4aSElliott Hughes "languagesystem latn TRK; languagesystem DFLT dflt;", 393*e1fe3e4aSElliott Hughes ) 394*e1fe3e4aSElliott Hughes 395*e1fe3e4aSElliott Hughes def test_languagesystem_DFLT_not_preceding(self): 396*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 397*e1fe3e4aSElliott Hughes FeatureLibError, 398*e1fe3e4aSElliott Hughes 'languagesystems using the "DFLT" script tag must ' 399*e1fe3e4aSElliott Hughes "precede all other languagesystems", 400*e1fe3e4aSElliott Hughes self.build, 401*e1fe3e4aSElliott Hughes "languagesystem DFLT dflt; " 402*e1fe3e4aSElliott Hughes "languagesystem latn dflt; " 403*e1fe3e4aSElliott Hughes "languagesystem DFLT fooo; ", 404*e1fe3e4aSElliott Hughes ) 405*e1fe3e4aSElliott Hughes 406*e1fe3e4aSElliott Hughes def test_script(self): 407*e1fe3e4aSElliott Hughes builder = Builder(makeTTFont(), (None, None)) 408*e1fe3e4aSElliott Hughes builder.start_feature(location=None, name="test") 409*e1fe3e4aSElliott Hughes builder.set_script(location=None, script="cyrl") 410*e1fe3e4aSElliott Hughes self.assertEqual(builder.language_systems, {("cyrl", "dflt")}) 411*e1fe3e4aSElliott Hughes 412*e1fe3e4aSElliott Hughes def test_script_in_aalt_feature(self): 413*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 414*e1fe3e4aSElliott Hughes FeatureLibError, 415*e1fe3e4aSElliott Hughes 'Script statements are not allowed within "feature aalt"', 416*e1fe3e4aSElliott Hughes self.build, 417*e1fe3e4aSElliott Hughes "feature aalt { script latn; } aalt;", 418*e1fe3e4aSElliott Hughes ) 419*e1fe3e4aSElliott Hughes 420*e1fe3e4aSElliott Hughes def test_script_in_size_feature(self): 421*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 422*e1fe3e4aSElliott Hughes FeatureLibError, 423*e1fe3e4aSElliott Hughes 'Script statements are not allowed within "feature size"', 424*e1fe3e4aSElliott Hughes self.build, 425*e1fe3e4aSElliott Hughes "feature size { script latn; } size;", 426*e1fe3e4aSElliott Hughes ) 427*e1fe3e4aSElliott Hughes 428*e1fe3e4aSElliott Hughes def test_script_in_standalone_lookup(self): 429*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 430*e1fe3e4aSElliott Hughes FeatureLibError, 431*e1fe3e4aSElliott Hughes "Script statements are not allowed within standalone lookup blocks", 432*e1fe3e4aSElliott Hughes self.build, 433*e1fe3e4aSElliott Hughes "lookup test { script latn; } test;", 434*e1fe3e4aSElliott Hughes ) 435*e1fe3e4aSElliott Hughes 436*e1fe3e4aSElliott Hughes def test_language(self): 437*e1fe3e4aSElliott Hughes builder = Builder(makeTTFont(), (None, None)) 438*e1fe3e4aSElliott Hughes builder.add_language_system(None, "latn", "FRA ") 439*e1fe3e4aSElliott Hughes builder.start_feature(location=None, name="test") 440*e1fe3e4aSElliott Hughes builder.set_script(location=None, script="cyrl") 441*e1fe3e4aSElliott Hughes builder.set_language( 442*e1fe3e4aSElliott Hughes location=None, language="RUS ", include_default=False, required=False 443*e1fe3e4aSElliott Hughes ) 444*e1fe3e4aSElliott Hughes self.assertEqual(builder.language_systems, {("cyrl", "RUS ")}) 445*e1fe3e4aSElliott Hughes builder.set_language( 446*e1fe3e4aSElliott Hughes location=None, language="BGR ", include_default=True, required=False 447*e1fe3e4aSElliott Hughes ) 448*e1fe3e4aSElliott Hughes self.assertEqual(builder.language_systems, {("cyrl", "BGR ")}) 449*e1fe3e4aSElliott Hughes builder.start_feature(location=None, name="test2") 450*e1fe3e4aSElliott Hughes self.assertEqual(builder.language_systems, {("latn", "FRA ")}) 451*e1fe3e4aSElliott Hughes 452*e1fe3e4aSElliott Hughes def test_language_in_aalt_feature(self): 453*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 454*e1fe3e4aSElliott Hughes FeatureLibError, 455*e1fe3e4aSElliott Hughes 'Language statements are not allowed within "feature aalt"', 456*e1fe3e4aSElliott Hughes self.build, 457*e1fe3e4aSElliott Hughes "feature aalt { language FRA; } aalt;", 458*e1fe3e4aSElliott Hughes ) 459*e1fe3e4aSElliott Hughes 460*e1fe3e4aSElliott Hughes def test_language_in_size_feature(self): 461*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 462*e1fe3e4aSElliott Hughes FeatureLibError, 463*e1fe3e4aSElliott Hughes 'Language statements are not allowed within "feature size"', 464*e1fe3e4aSElliott Hughes self.build, 465*e1fe3e4aSElliott Hughes "feature size { language FRA; } size;", 466*e1fe3e4aSElliott Hughes ) 467*e1fe3e4aSElliott Hughes 468*e1fe3e4aSElliott Hughes def test_language_in_standalone_lookup(self): 469*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 470*e1fe3e4aSElliott Hughes FeatureLibError, 471*e1fe3e4aSElliott Hughes "Language statements are not allowed within standalone lookup blocks", 472*e1fe3e4aSElliott Hughes self.build, 473*e1fe3e4aSElliott Hughes "lookup test { language FRA; } test;", 474*e1fe3e4aSElliott Hughes ) 475*e1fe3e4aSElliott Hughes 476*e1fe3e4aSElliott Hughes def test_language_required_duplicate(self): 477*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 478*e1fe3e4aSElliott Hughes FeatureLibError, 479*e1fe3e4aSElliott Hughes r"Language FRA \(script latn\) has already specified " 480*e1fe3e4aSElliott Hughes "feature scmp as its required feature", 481*e1fe3e4aSElliott Hughes self.build, 482*e1fe3e4aSElliott Hughes "feature scmp {" 483*e1fe3e4aSElliott Hughes " script latn;" 484*e1fe3e4aSElliott Hughes " language FRA required;" 485*e1fe3e4aSElliott Hughes " language DEU required;" 486*e1fe3e4aSElliott Hughes " substitute [a-z] by [A.sc-Z.sc];" 487*e1fe3e4aSElliott Hughes "} scmp;" 488*e1fe3e4aSElliott Hughes "feature test {" 489*e1fe3e4aSElliott Hughes " script latn;" 490*e1fe3e4aSElliott Hughes " language FRA required;" 491*e1fe3e4aSElliott Hughes " substitute [a-z] by [A.sc-Z.sc];" 492*e1fe3e4aSElliott Hughes "} test;", 493*e1fe3e4aSElliott Hughes ) 494*e1fe3e4aSElliott Hughes 495*e1fe3e4aSElliott Hughes def test_lookup_already_defined(self): 496*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 497*e1fe3e4aSElliott Hughes FeatureLibError, 498*e1fe3e4aSElliott Hughes 'Lookup "foo" has already been defined', 499*e1fe3e4aSElliott Hughes self.build, 500*e1fe3e4aSElliott Hughes "lookup foo {} foo; lookup foo {} foo;", 501*e1fe3e4aSElliott Hughes ) 502*e1fe3e4aSElliott Hughes 503*e1fe3e4aSElliott Hughes def test_lookup_multiple_flags(self): 504*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 505*e1fe3e4aSElliott Hughes FeatureLibError, 506*e1fe3e4aSElliott Hughes "Within a named lookup block, all rules must be " 507*e1fe3e4aSElliott Hughes "of the same lookup type and flag", 508*e1fe3e4aSElliott Hughes self.build, 509*e1fe3e4aSElliott Hughes "lookup foo {" 510*e1fe3e4aSElliott Hughes " lookupflag 1;" 511*e1fe3e4aSElliott Hughes " sub f i by f_i;" 512*e1fe3e4aSElliott Hughes " lookupflag 2;" 513*e1fe3e4aSElliott Hughes " sub f f i by f_f_i;" 514*e1fe3e4aSElliott Hughes "} foo;", 515*e1fe3e4aSElliott Hughes ) 516*e1fe3e4aSElliott Hughes 517*e1fe3e4aSElliott Hughes def test_lookup_multiple_types(self): 518*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 519*e1fe3e4aSElliott Hughes FeatureLibError, 520*e1fe3e4aSElliott Hughes "Within a named lookup block, all rules must be " 521*e1fe3e4aSElliott Hughes "of the same lookup type and flag", 522*e1fe3e4aSElliott Hughes self.build, 523*e1fe3e4aSElliott Hughes "lookup foo {" 524*e1fe3e4aSElliott Hughes " sub f f i by f_f_i;" 525*e1fe3e4aSElliott Hughes " sub A from [A.alt1 A.alt2];" 526*e1fe3e4aSElliott Hughes "} foo;", 527*e1fe3e4aSElliott Hughes ) 528*e1fe3e4aSElliott Hughes 529*e1fe3e4aSElliott Hughes def test_lookup_inside_feature_aalt(self): 530*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 531*e1fe3e4aSElliott Hughes FeatureLibError, 532*e1fe3e4aSElliott Hughes "Lookup blocks cannot be placed inside 'aalt' features", 533*e1fe3e4aSElliott Hughes self.build, 534*e1fe3e4aSElliott Hughes "feature aalt {lookup L {} L;} aalt;", 535*e1fe3e4aSElliott Hughes ) 536*e1fe3e4aSElliott Hughes 537*e1fe3e4aSElliott Hughes def test_chain_subst_refrences_GPOS_looup(self): 538*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 539*e1fe3e4aSElliott Hughes FeatureLibError, 540*e1fe3e4aSElliott Hughes "Missing index of the specified lookup, might be a positioning lookup", 541*e1fe3e4aSElliott Hughes self.build, 542*e1fe3e4aSElliott Hughes "lookup dummy { pos a 50; } dummy;" 543*e1fe3e4aSElliott Hughes "feature test {" 544*e1fe3e4aSElliott Hughes " sub a' lookup dummy b;" 545*e1fe3e4aSElliott Hughes "} test;", 546*e1fe3e4aSElliott Hughes ) 547*e1fe3e4aSElliott Hughes 548*e1fe3e4aSElliott Hughes def test_chain_pos_refrences_GSUB_looup(self): 549*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 550*e1fe3e4aSElliott Hughes FeatureLibError, 551*e1fe3e4aSElliott Hughes "Missing index of the specified lookup, might be a substitution lookup", 552*e1fe3e4aSElliott Hughes self.build, 553*e1fe3e4aSElliott Hughes "lookup dummy { sub a by A; } dummy;" 554*e1fe3e4aSElliott Hughes "feature test {" 555*e1fe3e4aSElliott Hughes " pos a' lookup dummy b;" 556*e1fe3e4aSElliott Hughes "} test;", 557*e1fe3e4aSElliott Hughes ) 558*e1fe3e4aSElliott Hughes 559*e1fe3e4aSElliott Hughes def test_STAT_elidedfallbackname_already_defined(self): 560*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 561*e1fe3e4aSElliott Hughes FeatureLibError, 562*e1fe3e4aSElliott Hughes "ElidedFallbackName is already set.", 563*e1fe3e4aSElliott Hughes self.build, 564*e1fe3e4aSElliott Hughes "table name {" 565*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 566*e1fe3e4aSElliott Hughes "} name;" 567*e1fe3e4aSElliott Hughes "table STAT {" 568*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 569*e1fe3e4aSElliott Hughes " ElidedFallbackNameID 256;" 570*e1fe3e4aSElliott Hughes "} STAT;", 571*e1fe3e4aSElliott Hughes ) 572*e1fe3e4aSElliott Hughes 573*e1fe3e4aSElliott Hughes def test_STAT_elidedfallbackname_set_twice(self): 574*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 575*e1fe3e4aSElliott Hughes FeatureLibError, 576*e1fe3e4aSElliott Hughes "ElidedFallbackName is already set.", 577*e1fe3e4aSElliott Hughes self.build, 578*e1fe3e4aSElliott Hughes "table name {" 579*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 580*e1fe3e4aSElliott Hughes "} name;" 581*e1fe3e4aSElliott Hughes "table STAT {" 582*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 583*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Italic"; };' 584*e1fe3e4aSElliott Hughes "} STAT;", 585*e1fe3e4aSElliott Hughes ) 586*e1fe3e4aSElliott Hughes 587*e1fe3e4aSElliott Hughes def test_STAT_elidedfallbacknameID_already_defined(self): 588*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 589*e1fe3e4aSElliott Hughes FeatureLibError, 590*e1fe3e4aSElliott Hughes "ElidedFallbackNameID is already set.", 591*e1fe3e4aSElliott Hughes self.build, 592*e1fe3e4aSElliott Hughes "table name {" 593*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 594*e1fe3e4aSElliott Hughes "} name;" 595*e1fe3e4aSElliott Hughes "table STAT {" 596*e1fe3e4aSElliott Hughes " ElidedFallbackNameID 256;" 597*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 598*e1fe3e4aSElliott Hughes "} STAT;", 599*e1fe3e4aSElliott Hughes ) 600*e1fe3e4aSElliott Hughes 601*e1fe3e4aSElliott Hughes def test_STAT_elidedfallbacknameID_not_in_name_table(self): 602*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 603*e1fe3e4aSElliott Hughes FeatureLibError, 604*e1fe3e4aSElliott Hughes "ElidedFallbackNameID 256 points to a nameID that does not " 605*e1fe3e4aSElliott Hughes 'exist in the "name" table', 606*e1fe3e4aSElliott Hughes self.build, 607*e1fe3e4aSElliott Hughes "table name {" 608*e1fe3e4aSElliott Hughes ' nameid 257 "Roman"; ' 609*e1fe3e4aSElliott Hughes "} name;" 610*e1fe3e4aSElliott Hughes "table STAT {" 611*e1fe3e4aSElliott Hughes " ElidedFallbackNameID 256;" 612*e1fe3e4aSElliott Hughes ' DesignAxis opsz 1 { name "Optical Size"; };' 613*e1fe3e4aSElliott Hughes "} STAT;", 614*e1fe3e4aSElliott Hughes ) 615*e1fe3e4aSElliott Hughes 616*e1fe3e4aSElliott Hughes def test_STAT_design_axis_name(self): 617*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 618*e1fe3e4aSElliott Hughes FeatureLibError, 619*e1fe3e4aSElliott Hughes 'Expected "name"', 620*e1fe3e4aSElliott Hughes self.build, 621*e1fe3e4aSElliott Hughes "table name {" 622*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 623*e1fe3e4aSElliott Hughes "} name;" 624*e1fe3e4aSElliott Hughes "table STAT {" 625*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 626*e1fe3e4aSElliott Hughes ' DesignAxis opsz 0 { badtag "Optical Size"; };' 627*e1fe3e4aSElliott Hughes "} STAT;", 628*e1fe3e4aSElliott Hughes ) 629*e1fe3e4aSElliott Hughes 630*e1fe3e4aSElliott Hughes def test_STAT_duplicate_design_axis_name(self): 631*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 632*e1fe3e4aSElliott Hughes FeatureLibError, 633*e1fe3e4aSElliott Hughes 'DesignAxis already defined for tag "opsz".', 634*e1fe3e4aSElliott Hughes self.build, 635*e1fe3e4aSElliott Hughes "table name {" 636*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 637*e1fe3e4aSElliott Hughes "} name;" 638*e1fe3e4aSElliott Hughes "table STAT {" 639*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 640*e1fe3e4aSElliott Hughes ' DesignAxis opsz 0 { name "Optical Size"; };' 641*e1fe3e4aSElliott Hughes ' DesignAxis opsz 1 { name "Optical Size"; };' 642*e1fe3e4aSElliott Hughes "} STAT;", 643*e1fe3e4aSElliott Hughes ) 644*e1fe3e4aSElliott Hughes 645*e1fe3e4aSElliott Hughes def test_STAT_design_axis_duplicate_order(self): 646*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 647*e1fe3e4aSElliott Hughes FeatureLibError, 648*e1fe3e4aSElliott Hughes "DesignAxis already defined for axis number 0.", 649*e1fe3e4aSElliott Hughes self.build, 650*e1fe3e4aSElliott Hughes "table name {" 651*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 652*e1fe3e4aSElliott Hughes "} name;" 653*e1fe3e4aSElliott Hughes "table STAT {" 654*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 655*e1fe3e4aSElliott Hughes ' DesignAxis opsz 0 { name "Optical Size"; };' 656*e1fe3e4aSElliott Hughes ' DesignAxis wdth 0 { name "Width"; };' 657*e1fe3e4aSElliott Hughes " AxisValue {" 658*e1fe3e4aSElliott Hughes " location opsz 8;" 659*e1fe3e4aSElliott Hughes " location wdth 400;" 660*e1fe3e4aSElliott Hughes ' name "Caption";' 661*e1fe3e4aSElliott Hughes " };" 662*e1fe3e4aSElliott Hughes "} STAT;", 663*e1fe3e4aSElliott Hughes ) 664*e1fe3e4aSElliott Hughes 665*e1fe3e4aSElliott Hughes def test_STAT_undefined_tag(self): 666*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 667*e1fe3e4aSElliott Hughes FeatureLibError, 668*e1fe3e4aSElliott Hughes "DesignAxis not defined for wdth.", 669*e1fe3e4aSElliott Hughes self.build, 670*e1fe3e4aSElliott Hughes "table name {" 671*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 672*e1fe3e4aSElliott Hughes "} name;" 673*e1fe3e4aSElliott Hughes "table STAT {" 674*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 675*e1fe3e4aSElliott Hughes ' DesignAxis opsz 0 { name "Optical Size"; };' 676*e1fe3e4aSElliott Hughes " AxisValue { " 677*e1fe3e4aSElliott Hughes " location wdth 125; " 678*e1fe3e4aSElliott Hughes ' name "Wide"; ' 679*e1fe3e4aSElliott Hughes " };" 680*e1fe3e4aSElliott Hughes "} STAT;", 681*e1fe3e4aSElliott Hughes ) 682*e1fe3e4aSElliott Hughes 683*e1fe3e4aSElliott Hughes def test_STAT_axis_value_format4(self): 684*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 685*e1fe3e4aSElliott Hughes FeatureLibError, 686*e1fe3e4aSElliott Hughes "Axis tag wdth already defined.", 687*e1fe3e4aSElliott Hughes self.build, 688*e1fe3e4aSElliott Hughes "table name {" 689*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 690*e1fe3e4aSElliott Hughes "} name;" 691*e1fe3e4aSElliott Hughes "table STAT {" 692*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 693*e1fe3e4aSElliott Hughes ' DesignAxis opsz 0 { name "Optical Size"; };' 694*e1fe3e4aSElliott Hughes ' DesignAxis wdth 1 { name "Width"; };' 695*e1fe3e4aSElliott Hughes ' DesignAxis wght 2 { name "Weight"; };' 696*e1fe3e4aSElliott Hughes " AxisValue { " 697*e1fe3e4aSElliott Hughes " location opsz 8; " 698*e1fe3e4aSElliott Hughes " location wdth 125; " 699*e1fe3e4aSElliott Hughes " location wdth 125; " 700*e1fe3e4aSElliott Hughes " location wght 500; " 701*e1fe3e4aSElliott Hughes ' name "Caption Medium Wide"; ' 702*e1fe3e4aSElliott Hughes " };" 703*e1fe3e4aSElliott Hughes "} STAT;", 704*e1fe3e4aSElliott Hughes ) 705*e1fe3e4aSElliott Hughes 706*e1fe3e4aSElliott Hughes def test_STAT_duplicate_axis_value_record(self): 707*e1fe3e4aSElliott Hughes # Test for Duplicate AxisValueRecords even when the definition order 708*e1fe3e4aSElliott Hughes # is different. 709*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 710*e1fe3e4aSElliott Hughes FeatureLibError, 711*e1fe3e4aSElliott Hughes "An AxisValueRecord with these values is already defined.", 712*e1fe3e4aSElliott Hughes self.build, 713*e1fe3e4aSElliott Hughes "table name {" 714*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 715*e1fe3e4aSElliott Hughes "} name;" 716*e1fe3e4aSElliott Hughes "table STAT {" 717*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; };' 718*e1fe3e4aSElliott Hughes ' DesignAxis opsz 0 { name "Optical Size"; };' 719*e1fe3e4aSElliott Hughes ' DesignAxis wdth 1 { name "Width"; };' 720*e1fe3e4aSElliott Hughes " AxisValue {" 721*e1fe3e4aSElliott Hughes " location opsz 8;" 722*e1fe3e4aSElliott Hughes " location wdth 400;" 723*e1fe3e4aSElliott Hughes ' name "Caption";' 724*e1fe3e4aSElliott Hughes " };" 725*e1fe3e4aSElliott Hughes " AxisValue {" 726*e1fe3e4aSElliott Hughes " location wdth 400;" 727*e1fe3e4aSElliott Hughes " location opsz 8;" 728*e1fe3e4aSElliott Hughes ' name "Caption";' 729*e1fe3e4aSElliott Hughes " };" 730*e1fe3e4aSElliott Hughes "} STAT;", 731*e1fe3e4aSElliott Hughes ) 732*e1fe3e4aSElliott Hughes 733*e1fe3e4aSElliott Hughes def test_STAT_axis_value_missing_location(self): 734*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 735*e1fe3e4aSElliott Hughes FeatureLibError, 736*e1fe3e4aSElliott Hughes 'Expected "Axis location"', 737*e1fe3e4aSElliott Hughes self.build, 738*e1fe3e4aSElliott Hughes "table name {" 739*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 740*e1fe3e4aSElliott Hughes "} name;" 741*e1fe3e4aSElliott Hughes "table STAT {" 742*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; ' 743*e1fe3e4aSElliott Hughes "};" 744*e1fe3e4aSElliott Hughes ' DesignAxis opsz 0 { name "Optical Size"; };' 745*e1fe3e4aSElliott Hughes " AxisValue { " 746*e1fe3e4aSElliott Hughes ' name "Wide"; ' 747*e1fe3e4aSElliott Hughes " };" 748*e1fe3e4aSElliott Hughes "} STAT;", 749*e1fe3e4aSElliott Hughes ) 750*e1fe3e4aSElliott Hughes 751*e1fe3e4aSElliott Hughes def test_STAT_invalid_location_tag(self): 752*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 753*e1fe3e4aSElliott Hughes FeatureLibError, 754*e1fe3e4aSElliott Hughes "Tags cannot be longer than 4 characters", 755*e1fe3e4aSElliott Hughes self.build, 756*e1fe3e4aSElliott Hughes "table name {" 757*e1fe3e4aSElliott Hughes ' nameid 256 "Roman"; ' 758*e1fe3e4aSElliott Hughes "} name;" 759*e1fe3e4aSElliott Hughes "table STAT {" 760*e1fe3e4aSElliott Hughes ' ElidedFallbackName { name "Roman"; ' 761*e1fe3e4aSElliott Hughes ' name 3 1 0x0411 "ローマン"; }; ' 762*e1fe3e4aSElliott Hughes ' DesignAxis width 0 { name "Width"; };' 763*e1fe3e4aSElliott Hughes "} STAT;", 764*e1fe3e4aSElliott Hughes ) 765*e1fe3e4aSElliott Hughes 766*e1fe3e4aSElliott Hughes def test_extensions(self): 767*e1fe3e4aSElliott Hughes class ast_BaseClass(ast.MarkClass): 768*e1fe3e4aSElliott Hughes def asFea(self, indent=""): 769*e1fe3e4aSElliott Hughes return "" 770*e1fe3e4aSElliott Hughes 771*e1fe3e4aSElliott Hughes class ast_BaseClassDefinition(ast.MarkClassDefinition): 772*e1fe3e4aSElliott Hughes def asFea(self, indent=""): 773*e1fe3e4aSElliott Hughes return "" 774*e1fe3e4aSElliott Hughes 775*e1fe3e4aSElliott Hughes class ast_MarkBasePosStatement(ast.MarkBasePosStatement): 776*e1fe3e4aSElliott Hughes def asFea(self, indent=""): 777*e1fe3e4aSElliott Hughes if isinstance(self.base, ast.MarkClassName): 778*e1fe3e4aSElliott Hughes res = "" 779*e1fe3e4aSElliott Hughes for bcd in self.base.markClass.definitions: 780*e1fe3e4aSElliott Hughes if res != "": 781*e1fe3e4aSElliott Hughes res += "\n{}".format(indent) 782*e1fe3e4aSElliott Hughes res += "pos base {} {}".format( 783*e1fe3e4aSElliott Hughes bcd.glyphs.asFea(), bcd.anchor.asFea() 784*e1fe3e4aSElliott Hughes ) 785*e1fe3e4aSElliott Hughes for m in self.marks: 786*e1fe3e4aSElliott Hughes res += " mark @{}".format(m.name) 787*e1fe3e4aSElliott Hughes res += ";" 788*e1fe3e4aSElliott Hughes else: 789*e1fe3e4aSElliott Hughes res = "pos base {}".format(self.base.asFea()) 790*e1fe3e4aSElliott Hughes for a, m in self.marks: 791*e1fe3e4aSElliott Hughes res += " {} mark @{}".format(a.asFea(), m.name) 792*e1fe3e4aSElliott Hughes res += ";" 793*e1fe3e4aSElliott Hughes return res 794*e1fe3e4aSElliott Hughes 795*e1fe3e4aSElliott Hughes class testAst(object): 796*e1fe3e4aSElliott Hughes MarkBasePosStatement = ast_MarkBasePosStatement 797*e1fe3e4aSElliott Hughes 798*e1fe3e4aSElliott Hughes def __getattr__(self, name): 799*e1fe3e4aSElliott Hughes return getattr(ast, name) 800*e1fe3e4aSElliott Hughes 801*e1fe3e4aSElliott Hughes class testParser(Parser): 802*e1fe3e4aSElliott Hughes def parse_position_base_(self, enumerated, vertical): 803*e1fe3e4aSElliott Hughes location = self.cur_token_location_ 804*e1fe3e4aSElliott Hughes self.expect_keyword_("base") 805*e1fe3e4aSElliott Hughes if enumerated: 806*e1fe3e4aSElliott Hughes raise FeatureLibError( 807*e1fe3e4aSElliott Hughes '"enumerate" is not allowed with ' 808*e1fe3e4aSElliott Hughes "mark-to-base attachment positioning", 809*e1fe3e4aSElliott Hughes location, 810*e1fe3e4aSElliott Hughes ) 811*e1fe3e4aSElliott Hughes base = self.parse_glyphclass_(accept_glyphname=True) 812*e1fe3e4aSElliott Hughes if self.next_token_ == "<": 813*e1fe3e4aSElliott Hughes marks = self.parse_anchor_marks_() 814*e1fe3e4aSElliott Hughes else: 815*e1fe3e4aSElliott Hughes marks = [] 816*e1fe3e4aSElliott Hughes while self.next_token_ == "mark": 817*e1fe3e4aSElliott Hughes self.expect_keyword_("mark") 818*e1fe3e4aSElliott Hughes m = self.expect_markClass_reference_() 819*e1fe3e4aSElliott Hughes marks.append(m) 820*e1fe3e4aSElliott Hughes self.expect_symbol_(";") 821*e1fe3e4aSElliott Hughes return self.ast.MarkBasePosStatement(base, marks, location=location) 822*e1fe3e4aSElliott Hughes 823*e1fe3e4aSElliott Hughes def parseBaseClass(self): 824*e1fe3e4aSElliott Hughes if not hasattr(self.doc_, "baseClasses"): 825*e1fe3e4aSElliott Hughes self.doc_.baseClasses = {} 826*e1fe3e4aSElliott Hughes location = self.cur_token_location_ 827*e1fe3e4aSElliott Hughes glyphs = self.parse_glyphclass_(accept_glyphname=True) 828*e1fe3e4aSElliott Hughes anchor = self.parse_anchor_() 829*e1fe3e4aSElliott Hughes name = self.expect_class_name_() 830*e1fe3e4aSElliott Hughes self.expect_symbol_(";") 831*e1fe3e4aSElliott Hughes baseClass = self.doc_.baseClasses.get(name) 832*e1fe3e4aSElliott Hughes if baseClass is None: 833*e1fe3e4aSElliott Hughes baseClass = ast_BaseClass(name) 834*e1fe3e4aSElliott Hughes self.doc_.baseClasses[name] = baseClass 835*e1fe3e4aSElliott Hughes self.glyphclasses_.define(name, baseClass) 836*e1fe3e4aSElliott Hughes bcdef = ast_BaseClassDefinition( 837*e1fe3e4aSElliott Hughes baseClass, anchor, glyphs, location=location 838*e1fe3e4aSElliott Hughes ) 839*e1fe3e4aSElliott Hughes baseClass.addDefinition(bcdef) 840*e1fe3e4aSElliott Hughes return bcdef 841*e1fe3e4aSElliott Hughes 842*e1fe3e4aSElliott Hughes extensions = {"baseClass": lambda s: s.parseBaseClass()} 843*e1fe3e4aSElliott Hughes ast = testAst() 844*e1fe3e4aSElliott Hughes 845*e1fe3e4aSElliott Hughes self.check_fea2fea_file( 846*e1fe3e4aSElliott Hughes "baseClass.feax", base="baseClass.fea", parser=testParser 847*e1fe3e4aSElliott Hughes ) 848*e1fe3e4aSElliott Hughes 849*e1fe3e4aSElliott Hughes def test_markClass_same_glyph_redefined(self): 850*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 851*e1fe3e4aSElliott Hughes FeatureLibError, 852*e1fe3e4aSElliott Hughes "Glyph acute already defined", 853*e1fe3e4aSElliott Hughes self.build, 854*e1fe3e4aSElliott Hughes "markClass [acute] <anchor 350 0> @TOP_MARKS;" * 2, 855*e1fe3e4aSElliott Hughes ) 856*e1fe3e4aSElliott Hughes 857*e1fe3e4aSElliott Hughes def test_markClass_same_glyph_multiple_classes(self): 858*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 859*e1fe3e4aSElliott Hughes FeatureLibError, 860*e1fe3e4aSElliott Hughes "Glyph uni0327 cannot be in both @ogonek and @cedilla", 861*e1fe3e4aSElliott Hughes self.build, 862*e1fe3e4aSElliott Hughes "feature mark {" 863*e1fe3e4aSElliott Hughes " markClass [uni0327 uni0328] <anchor 0 0> @ogonek;" 864*e1fe3e4aSElliott Hughes " pos base [a] <anchor 399 0> mark @ogonek;" 865*e1fe3e4aSElliott Hughes " markClass [uni0327] <anchor 0 0> @cedilla;" 866*e1fe3e4aSElliott Hughes " pos base [a] <anchor 244 0> mark @cedilla;" 867*e1fe3e4aSElliott Hughes "} mark;", 868*e1fe3e4aSElliott Hughes ) 869*e1fe3e4aSElliott Hughes 870*e1fe3e4aSElliott Hughes def test_build_specific_tables(self): 871*e1fe3e4aSElliott Hughes features = "feature liga {sub f i by f_i;} liga;" 872*e1fe3e4aSElliott Hughes font = self.build(features) 873*e1fe3e4aSElliott Hughes assert "GSUB" in font 874*e1fe3e4aSElliott Hughes 875*e1fe3e4aSElliott Hughes font2 = self.build(features, tables=set()) 876*e1fe3e4aSElliott Hughes assert "GSUB" not in font2 877*e1fe3e4aSElliott Hughes 878*e1fe3e4aSElliott Hughes def test_build_unsupported_tables(self): 879*e1fe3e4aSElliott Hughes self.assertRaises(NotImplementedError, self.build, "", tables={"FOO"}) 880*e1fe3e4aSElliott Hughes 881*e1fe3e4aSElliott Hughes def test_build_pre_parsed_ast_featurefile(self): 882*e1fe3e4aSElliott Hughes f = StringIO("feature liga {sub f i by f_i;} liga;") 883*e1fe3e4aSElliott Hughes tree = Parser(f).parse() 884*e1fe3e4aSElliott Hughes font = makeTTFont() 885*e1fe3e4aSElliott Hughes addOpenTypeFeatures(font, tree) 886*e1fe3e4aSElliott Hughes assert "GSUB" in font 887*e1fe3e4aSElliott Hughes 888*e1fe3e4aSElliott Hughes def test_unsupported_subtable_break(self): 889*e1fe3e4aSElliott Hughes logger = logging.getLogger("fontTools.otlLib.builder") 890*e1fe3e4aSElliott Hughes with CapturingLogHandler(logger, level="WARNING") as captor: 891*e1fe3e4aSElliott Hughes self.build( 892*e1fe3e4aSElliott Hughes "feature test {" 893*e1fe3e4aSElliott Hughes " pos a 10;" 894*e1fe3e4aSElliott Hughes " subtable;" 895*e1fe3e4aSElliott Hughes " pos b 10;" 896*e1fe3e4aSElliott Hughes "} test;" 897*e1fe3e4aSElliott Hughes ) 898*e1fe3e4aSElliott Hughes 899*e1fe3e4aSElliott Hughes captor.assertRegex( 900*e1fe3e4aSElliott Hughes '<features>:1:32: unsupported "subtable" statement for lookup type' 901*e1fe3e4aSElliott Hughes ) 902*e1fe3e4aSElliott Hughes 903*e1fe3e4aSElliott Hughes def test_skip_featureNames_if_no_name_table(self): 904*e1fe3e4aSElliott Hughes features = ( 905*e1fe3e4aSElliott Hughes "feature ss01 {" 906*e1fe3e4aSElliott Hughes " featureNames {" 907*e1fe3e4aSElliott Hughes ' name "ignored as we request to skip name table";' 908*e1fe3e4aSElliott Hughes " };" 909*e1fe3e4aSElliott Hughes " sub A by A.alt1;" 910*e1fe3e4aSElliott Hughes "} ss01;" 911*e1fe3e4aSElliott Hughes ) 912*e1fe3e4aSElliott Hughes font = self.build(features, tables=["GSUB"]) 913*e1fe3e4aSElliott Hughes self.assertIn("GSUB", font) 914*e1fe3e4aSElliott Hughes self.assertNotIn("name", font) 915*e1fe3e4aSElliott Hughes 916*e1fe3e4aSElliott Hughes def test_singlePos_multiplePositionsForSameGlyph(self): 917*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 918*e1fe3e4aSElliott Hughes FeatureLibError, 919*e1fe3e4aSElliott Hughes "Already defined different position for glyph", 920*e1fe3e4aSElliott Hughes self.build, 921*e1fe3e4aSElliott Hughes "lookup foo {" " pos A -45; " " pos A 45; " "} foo;", 922*e1fe3e4aSElliott Hughes ) 923*e1fe3e4aSElliott Hughes 924*e1fe3e4aSElliott Hughes def test_pairPos_enumRuleOverridenBySinglePair_DEBUG(self): 925*e1fe3e4aSElliott Hughes logger = logging.getLogger("fontTools.otlLib.builder") 926*e1fe3e4aSElliott Hughes with CapturingLogHandler(logger, "DEBUG") as captor: 927*e1fe3e4aSElliott Hughes self.build( 928*e1fe3e4aSElliott Hughes "feature test {" 929*e1fe3e4aSElliott Hughes " enum pos A [V Y] -80;" 930*e1fe3e4aSElliott Hughes " pos A V -75;" 931*e1fe3e4aSElliott Hughes "} test;" 932*e1fe3e4aSElliott Hughes ) 933*e1fe3e4aSElliott Hughes captor.assertRegex("Already defined position for pair A V at") 934*e1fe3e4aSElliott Hughes 935*e1fe3e4aSElliott Hughes def test_ignore_empty_lookup_block(self): 936*e1fe3e4aSElliott Hughes # https://github.com/fonttools/fonttools/pull/2277 937*e1fe3e4aSElliott Hughes font = self.build( 938*e1fe3e4aSElliott Hughes "lookup EMPTY { ; } EMPTY;" "feature ss01 { lookup EMPTY; } ss01;" 939*e1fe3e4aSElliott Hughes ) 940*e1fe3e4aSElliott Hughes assert "GPOS" not in font 941*e1fe3e4aSElliott Hughes assert "GSUB" not in font 942*e1fe3e4aSElliott Hughes 943*e1fe3e4aSElliott Hughes def test_disable_empty_classes(self): 944*e1fe3e4aSElliott Hughes for test in [ 945*e1fe3e4aSElliott Hughes "sub a by c []", 946*e1fe3e4aSElliott Hughes "sub f f [] by f", 947*e1fe3e4aSElliott Hughes "ignore sub a []'", 948*e1fe3e4aSElliott Hughes "ignore sub [] a'", 949*e1fe3e4aSElliott Hughes "sub a []' by b", 950*e1fe3e4aSElliott Hughes "sub [] a' by b", 951*e1fe3e4aSElliott Hughes "rsub [] by a", 952*e1fe3e4aSElliott Hughes "pos [] 120", 953*e1fe3e4aSElliott Hughes "pos a [] 120", 954*e1fe3e4aSElliott Hughes "enum pos a [] 120", 955*e1fe3e4aSElliott Hughes "pos cursive [] <anchor NULL> <anchor NULL>", 956*e1fe3e4aSElliott Hughes "pos base [] <anchor NULL> mark @TOPMARKS", 957*e1fe3e4aSElliott Hughes "pos ligature [] <anchor NULL> mark @TOPMARKS", 958*e1fe3e4aSElliott Hughes "pos mark [] <anchor NULL> mark @TOPMARKS", 959*e1fe3e4aSElliott Hughes "ignore pos a []'", 960*e1fe3e4aSElliott Hughes "ignore pos [] a'", 961*e1fe3e4aSElliott Hughes ]: 962*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 963*e1fe3e4aSElliott Hughes FeatureLibError, 964*e1fe3e4aSElliott Hughes "Empty ", 965*e1fe3e4aSElliott Hughes self.build, 966*e1fe3e4aSElliott Hughes f"markClass a <anchor 150 -10> @TOPMARKS; lookup foo {{ {test}; }} foo;", 967*e1fe3e4aSElliott Hughes ) 968*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 969*e1fe3e4aSElliott Hughes FeatureLibError, 970*e1fe3e4aSElliott Hughes "Empty glyph class in mark class definition", 971*e1fe3e4aSElliott Hughes self.build, 972*e1fe3e4aSElliott Hughes "markClass [] <anchor 150 -10> @TOPMARKS;", 973*e1fe3e4aSElliott Hughes ) 974*e1fe3e4aSElliott Hughes self.assertRaisesRegex( 975*e1fe3e4aSElliott Hughes FeatureLibError, 976*e1fe3e4aSElliott Hughes 'Expected a glyph class with 1 elements after "by", but found a glyph class with 0 elements', 977*e1fe3e4aSElliott Hughes self.build, 978*e1fe3e4aSElliott Hughes "feature test { sub a by []; test};", 979*e1fe3e4aSElliott Hughes ) 980*e1fe3e4aSElliott Hughes 981*e1fe3e4aSElliott Hughes def test_unmarked_ignore_statement(self): 982*e1fe3e4aSElliott Hughes name = "bug2949" 983*e1fe3e4aSElliott Hughes logger = logging.getLogger("fontTools.feaLib.parser") 984*e1fe3e4aSElliott Hughes with CapturingLogHandler(logger, level="WARNING") as captor: 985*e1fe3e4aSElliott Hughes self.check_feature_file(name) 986*e1fe3e4aSElliott Hughes self.check_fea2fea_file(name) 987*e1fe3e4aSElliott Hughes 988*e1fe3e4aSElliott Hughes for line, sub in {(3, "sub"), (8, "pos"), (13, "sub")}: 989*e1fe3e4aSElliott Hughes captor.assertRegex( 990*e1fe3e4aSElliott Hughes f'{name}.fea:{line}:12: Ambiguous "ignore {sub}", there should be least one marked glyph' 991*e1fe3e4aSElliott Hughes ) 992*e1fe3e4aSElliott Hughes 993*e1fe3e4aSElliott Hughes def test_conditionset_multiple_features(self): 994*e1fe3e4aSElliott Hughes """Test that using the same `conditionset` for multiple features reuses the 995*e1fe3e4aSElliott Hughes `FeatureVariationRecord`.""" 996*e1fe3e4aSElliott Hughes 997*e1fe3e4aSElliott Hughes features = """ 998*e1fe3e4aSElliott Hughes languagesystem DFLT dflt; 999*e1fe3e4aSElliott Hughes 1000*e1fe3e4aSElliott Hughes conditionset test { 1001*e1fe3e4aSElliott Hughes wght 600 1000; 1002*e1fe3e4aSElliott Hughes wdth 150 200; 1003*e1fe3e4aSElliott Hughes } test; 1004*e1fe3e4aSElliott Hughes 1005*e1fe3e4aSElliott Hughes variation ccmp test { 1006*e1fe3e4aSElliott Hughes sub e by a; 1007*e1fe3e4aSElliott Hughes } ccmp; 1008*e1fe3e4aSElliott Hughes 1009*e1fe3e4aSElliott Hughes variation rlig test { 1010*e1fe3e4aSElliott Hughes sub b by c; 1011*e1fe3e4aSElliott Hughes } rlig; 1012*e1fe3e4aSElliott Hughes """ 1013*e1fe3e4aSElliott Hughes 1014*e1fe3e4aSElliott Hughes def make_mock_vf(): 1015*e1fe3e4aSElliott Hughes font = makeTTFont() 1016*e1fe3e4aSElliott Hughes font["name"] = newTable("name") 1017*e1fe3e4aSElliott Hughes addFvar( 1018*e1fe3e4aSElliott Hughes font, 1019*e1fe3e4aSElliott Hughes [("wght", 0, 0, 1000, "Weight"), ("wdth", 100, 100, 200, "Width")], 1020*e1fe3e4aSElliott Hughes [], 1021*e1fe3e4aSElliott Hughes ) 1022*e1fe3e4aSElliott Hughes del font["name"] 1023*e1fe3e4aSElliott Hughes return font 1024*e1fe3e4aSElliott Hughes 1025*e1fe3e4aSElliott Hughes font = make_mock_vf() 1026*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString(font, features) 1027*e1fe3e4aSElliott Hughes 1028*e1fe3e4aSElliott Hughes table = font["GSUB"].table 1029*e1fe3e4aSElliott Hughes assert table.FeatureVariations.FeatureVariationCount == 1 1030*e1fe3e4aSElliott Hughes 1031*e1fe3e4aSElliott Hughes fvr = table.FeatureVariations.FeatureVariationRecord[0] 1032*e1fe3e4aSElliott Hughes assert fvr.FeatureTableSubstitution.SubstitutionCount == 2 1033*e1fe3e4aSElliott Hughes 1034*e1fe3e4aSElliott Hughes def test_condition_set_avar(self): 1035*e1fe3e4aSElliott Hughes """Test that the `avar` table is consulted when normalizing user-space 1036*e1fe3e4aSElliott Hughes values.""" 1037*e1fe3e4aSElliott Hughes 1038*e1fe3e4aSElliott Hughes features = """ 1039*e1fe3e4aSElliott Hughes languagesystem DFLT dflt; 1040*e1fe3e4aSElliott Hughes 1041*e1fe3e4aSElliott Hughes lookup conditional_sub { 1042*e1fe3e4aSElliott Hughes sub e by a; 1043*e1fe3e4aSElliott Hughes } conditional_sub; 1044*e1fe3e4aSElliott Hughes 1045*e1fe3e4aSElliott Hughes conditionset test { 1046*e1fe3e4aSElliott Hughes wght 600 1000; 1047*e1fe3e4aSElliott Hughes wdth 150 200; 1048*e1fe3e4aSElliott Hughes } test; 1049*e1fe3e4aSElliott Hughes 1050*e1fe3e4aSElliott Hughes variation rlig test { 1051*e1fe3e4aSElliott Hughes lookup conditional_sub; 1052*e1fe3e4aSElliott Hughes } rlig; 1053*e1fe3e4aSElliott Hughes """ 1054*e1fe3e4aSElliott Hughes 1055*e1fe3e4aSElliott Hughes def make_mock_vf(): 1056*e1fe3e4aSElliott Hughes font = makeTTFont() 1057*e1fe3e4aSElliott Hughes font["name"] = newTable("name") 1058*e1fe3e4aSElliott Hughes addFvar( 1059*e1fe3e4aSElliott Hughes font, 1060*e1fe3e4aSElliott Hughes [("wght", 0, 0, 1000, "Weight"), ("wdth", 100, 100, 200, "Width")], 1061*e1fe3e4aSElliott Hughes [], 1062*e1fe3e4aSElliott Hughes ) 1063*e1fe3e4aSElliott Hughes del font["name"] 1064*e1fe3e4aSElliott Hughes return font 1065*e1fe3e4aSElliott Hughes 1066*e1fe3e4aSElliott Hughes # Without `avar`: 1067*e1fe3e4aSElliott Hughes font = make_mock_vf() 1068*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString(font, features) 1069*e1fe3e4aSElliott Hughes condition_table = ( 1070*e1fe3e4aSElliott Hughes font.tables["GSUB"] 1071*e1fe3e4aSElliott Hughes .table.FeatureVariations.FeatureVariationRecord[0] 1072*e1fe3e4aSElliott Hughes .ConditionSet.ConditionTable 1073*e1fe3e4aSElliott Hughes ) 1074*e1fe3e4aSElliott Hughes # user-space wdth=150 and wght=600: 1075*e1fe3e4aSElliott Hughes assert condition_table[0].FilterRangeMinValue == 0.5 1076*e1fe3e4aSElliott Hughes assert condition_table[1].FilterRangeMinValue == 0.6 1077*e1fe3e4aSElliott Hughes 1078*e1fe3e4aSElliott Hughes # With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to 1079*e1fe3e4aSElliott Hughes # the right, but leaving the wdth axis alone: 1080*e1fe3e4aSElliott Hughes font = make_mock_vf() 1081*e1fe3e4aSElliott Hughes font["avar"] = newTable("avar") 1082*e1fe3e4aSElliott Hughes font["avar"].segments = {"wght": {-1.0: -1.0, 0.0: 0.0, 0.5: 0.625, 1.0: 1.0}} 1083*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString(font, features) 1084*e1fe3e4aSElliott Hughes condition_table = ( 1085*e1fe3e4aSElliott Hughes font.tables["GSUB"] 1086*e1fe3e4aSElliott Hughes .table.FeatureVariations.FeatureVariationRecord[0] 1087*e1fe3e4aSElliott Hughes .ConditionSet.ConditionTable 1088*e1fe3e4aSElliott Hughes ) 1089*e1fe3e4aSElliott Hughes # user-space wdth=150 as before and wght=600 shifted to the right: 1090*e1fe3e4aSElliott Hughes assert condition_table[0].FilterRangeMinValue == 0.5 1091*e1fe3e4aSElliott Hughes assert condition_table[1].FilterRangeMinValue == 0.7 1092*e1fe3e4aSElliott Hughes 1093*e1fe3e4aSElliott Hughes def test_variable_scalar_avar(self): 1094*e1fe3e4aSElliott Hughes """Test that the `avar` table is consulted when normalizing user-space 1095*e1fe3e4aSElliott Hughes values.""" 1096*e1fe3e4aSElliott Hughes 1097*e1fe3e4aSElliott Hughes features = """ 1098*e1fe3e4aSElliott Hughes languagesystem DFLT dflt; 1099*e1fe3e4aSElliott Hughes 1100*e1fe3e4aSElliott Hughes feature kern { 1101*e1fe3e4aSElliott Hughes pos cursive one <anchor 0 (wght=200:12 wght=900:22 wdth=150,wght=900:42)> <anchor NULL>; 1102*e1fe3e4aSElliott Hughes pos two <0 (wght=200:12 wght=900:22 wdth=150,wght=900:42) 0 0>; 1103*e1fe3e4aSElliott Hughes } kern; 1104*e1fe3e4aSElliott Hughes """ 1105*e1fe3e4aSElliott Hughes 1106*e1fe3e4aSElliott Hughes # Without `avar` (wght=200, wdth=100 is the default location): 1107*e1fe3e4aSElliott Hughes font = self.make_mock_vf() 1108*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString(font, features) 1109*e1fe3e4aSElliott Hughes 1110*e1fe3e4aSElliott Hughes var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList 1111*e1fe3e4aSElliott Hughes var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0] 1112*e1fe3e4aSElliott Hughes var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1] 1113*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875) 1114*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0) 1115*e1fe3e4aSElliott Hughes var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0] 1116*e1fe3e4aSElliott Hughes var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1] 1117*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875) 1118*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5) 1119*e1fe3e4aSElliott Hughes 1120*e1fe3e4aSElliott Hughes # With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to 1121*e1fe3e4aSElliott Hughes # the right, but leaving the wdth axis alone: 1122*e1fe3e4aSElliott Hughes font = self.make_mock_vf() 1123*e1fe3e4aSElliott Hughes font["avar"] = newTable("avar") 1124*e1fe3e4aSElliott Hughes font["avar"].segments = {"wght": {-1.0: -1.0, 0.0: 0.0, 0.5: 0.625, 1.0: 1.0}} 1125*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString(font, features) 1126*e1fe3e4aSElliott Hughes 1127*e1fe3e4aSElliott Hughes var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList 1128*e1fe3e4aSElliott Hughes var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0] 1129*e1fe3e4aSElliott Hughes var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1] 1130*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625) 1131*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0) 1132*e1fe3e4aSElliott Hughes var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0] 1133*e1fe3e4aSElliott Hughes var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1] 1134*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625) 1135*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5) 1136*e1fe3e4aSElliott Hughes 1137*e1fe3e4aSElliott Hughes def test_ligatureCaretByPos_variable_scalar(self): 1138*e1fe3e4aSElliott Hughes """Test that the `avar` table is consulted when normalizing user-space 1139*e1fe3e4aSElliott Hughes values.""" 1140*e1fe3e4aSElliott Hughes 1141*e1fe3e4aSElliott Hughes features = """ 1142*e1fe3e4aSElliott Hughes table GDEF { 1143*e1fe3e4aSElliott Hughes LigatureCaretByPos f_i (wght=200:400 wght=900:1000) 380; 1144*e1fe3e4aSElliott Hughes } GDEF; 1145*e1fe3e4aSElliott Hughes """ 1146*e1fe3e4aSElliott Hughes 1147*e1fe3e4aSElliott Hughes font = self.make_mock_vf() 1148*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString(font, features) 1149*e1fe3e4aSElliott Hughes 1150*e1fe3e4aSElliott Hughes table = font["GDEF"].table 1151*e1fe3e4aSElliott Hughes lig_glyph = table.LigCaretList.LigGlyph[0] 1152*e1fe3e4aSElliott Hughes assert lig_glyph.CaretValue[0].Format == 1 1153*e1fe3e4aSElliott Hughes assert lig_glyph.CaretValue[0].Coordinate == 380 1154*e1fe3e4aSElliott Hughes assert lig_glyph.CaretValue[1].Format == 3 1155*e1fe3e4aSElliott Hughes assert lig_glyph.CaretValue[1].Coordinate == 400 1156*e1fe3e4aSElliott Hughes 1157*e1fe3e4aSElliott Hughes var_region_list = table.VarStore.VarRegionList 1158*e1fe3e4aSElliott Hughes var_region_axis = var_region_list.Region[0].VarRegionAxis[0] 1159*e1fe3e4aSElliott Hughes assert self.get_region(var_region_axis) == (0.0, 0.875, 0.875) 1160*e1fe3e4aSElliott Hughes 1161*e1fe3e4aSElliott Hughes 1162*e1fe3e4aSElliott Hughesdef generate_feature_file_test(name): 1163*e1fe3e4aSElliott Hughes return lambda self: self.check_feature_file(name) 1164*e1fe3e4aSElliott Hughes 1165*e1fe3e4aSElliott Hughes 1166*e1fe3e4aSElliott Hughesfor name in BuilderTest.TEST_FEATURE_FILES: 1167*e1fe3e4aSElliott Hughes setattr(BuilderTest, "test_FeatureFile_%s" % name, generate_feature_file_test(name)) 1168*e1fe3e4aSElliott Hughes 1169*e1fe3e4aSElliott Hughes 1170*e1fe3e4aSElliott Hughesdef generate_fea2fea_file_test(name): 1171*e1fe3e4aSElliott Hughes return lambda self: self.check_fea2fea_file(name) 1172*e1fe3e4aSElliott Hughes 1173*e1fe3e4aSElliott Hughes 1174*e1fe3e4aSElliott Hughesfor name in BuilderTest.TEST_FEATURE_FILES: 1175*e1fe3e4aSElliott Hughes setattr( 1176*e1fe3e4aSElliott Hughes BuilderTest, 1177*e1fe3e4aSElliott Hughes "test_Fea2feaFile_{}".format(name), 1178*e1fe3e4aSElliott Hughes generate_fea2fea_file_test(name), 1179*e1fe3e4aSElliott Hughes ) 1180*e1fe3e4aSElliott Hughes 1181*e1fe3e4aSElliott Hughes 1182*e1fe3e4aSElliott Hughesif __name__ == "__main__": 1183*e1fe3e4aSElliott Hughes sys.exit(unittest.main()) 1184