2018-01-15 18:45:42 +00:00
|
|
|
from fontTools.misc.loggingTools import CapturingLogHandler
|
2021-10-28 11:58:54 +01:00
|
|
|
from fontTools.feaLib.builder import (
|
|
|
|
Builder,
|
|
|
|
addOpenTypeFeatures,
|
|
|
|
addOpenTypeFeaturesFromString,
|
|
|
|
)
|
2015-09-04 16:11:53 +02:00
|
|
|
from fontTools.feaLib.error import FeatureLibError
|
2021-10-28 11:58:54 +01:00
|
|
|
from fontTools.ttLib import TTFont, newTable
|
2016-12-20 10:10:29 +00:00
|
|
|
from fontTools.feaLib.parser import Parser
|
2017-01-04 16:18:50 +00:00
|
|
|
from fontTools.feaLib import ast
|
|
|
|
from fontTools.feaLib.lexer import Lexer
|
2021-10-28 11:58:54 +01:00
|
|
|
from fontTools.fontBuilder import addFvar
|
2015-09-04 15:06:11 +02:00
|
|
|
import difflib
|
2021-03-29 11:45:58 +02:00
|
|
|
from io import StringIO
|
2015-09-04 15:06:11 +02:00
|
|
|
import os
|
2020-08-11 21:21:30 -07:00
|
|
|
import re
|
2015-09-04 15:06:11 +02:00
|
|
|
import shutil
|
|
|
|
import sys
|
|
|
|
import tempfile
|
2018-01-15 18:45:42 +00:00
|
|
|
import logging
|
2015-09-04 15:06:11 +02:00
|
|
|
import unittest
|
2023-05-17 15:08:08 +01:00
|
|
|
import warnings
|
2015-09-04 15:06:11 +02:00
|
|
|
|
|
|
|
|
2015-12-04 11:04:37 +01:00
|
|
|
def makeTTFont():
|
2016-02-05 15:55:53 +01:00
|
|
|
glyphs = """
|
|
|
|
.notdef space slash fraction semicolon period comma ampersand
|
|
|
|
quotedblleft quotedblright quoteleft quoteright
|
|
|
|
zero one two three four five six seven eight nine
|
|
|
|
zero.oldstyle one.oldstyle two.oldstyle three.oldstyle
|
|
|
|
four.oldstyle five.oldstyle six.oldstyle seven.oldstyle
|
|
|
|
eight.oldstyle nine.oldstyle onequarter onehalf threequarters
|
|
|
|
onesuperior twosuperior threesuperior ordfeminine ordmasculine
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3
|
|
|
|
a.alt1 a.alt2 a.alt3 a.end b.alt c.mid d.alt d.mid
|
|
|
|
e.begin e.mid e.end m.begin n.end s.end z.end
|
|
|
|
Eng Eng.alt1 Eng.alt2 Eng.alt3
|
|
|
|
A.swash B.swash C.swash D.swash E.swash F.swash G.swash H.swash
|
|
|
|
I.swash J.swash K.swash L.swash M.swash N.swash O.swash P.swash
|
|
|
|
Q.swash R.swash S.swash T.swash U.swash V.swash W.swash X.swash
|
|
|
|
Y.swash Z.swash
|
|
|
|
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
|
2016-06-21 16:40:17 -07:00
|
|
|
a_n_d T_h T_h.swash germandbls ydieresis yacute breve
|
2016-02-05 15:55:53 +01:00
|
|
|
grave acute dieresis macron circumflex cedilla umlaut ogonek caron
|
|
|
|
damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial
|
2020-02-13 14:47:29 +00:00
|
|
|
by feature lookup sub table uni0327 uni0328 e.fina
|
2016-02-05 15:55:53 +01:00
|
|
|
""".split()
|
2020-04-03 19:40:22 -04:00
|
|
|
glyphs.extend("cid{:05d}".format(cid) for cid in range(800, 1001 + 1))
|
2015-12-04 11:04:37 +01:00
|
|
|
font = TTFont()
|
|
|
|
font.setGlyphOrder(glyphs)
|
|
|
|
return font
|
|
|
|
|
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
class BuilderTest(unittest.TestCase):
|
2017-01-16 09:36:10 +00:00
|
|
|
# Feature files in data/*.fea; output gets compared to data/*.ttx.
|
2016-01-14 08:37:37 +01:00
|
|
|
TEST_FEATURE_FILES = """
|
2020-04-03 19:40:22 -04:00
|
|
|
Attach cid_range enum markClass language_required
|
2016-01-14 08:37:37 +01:00
|
|
|
GlyphClassDef LigatureCaretByIndex LigatureCaretByPos
|
2016-02-06 12:13:03 +01:00
|
|
|
lookup lookupflag feature_aalt ignore_pos
|
2016-01-19 16:03:29 +01:00
|
|
|
GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8
|
2016-01-14 08:37:37 +01:00
|
|
|
GSUB_2 GSUB_3 GSUB_6 GSUB_8
|
2016-06-21 16:40:17 -07:00
|
|
|
spec4h1 spec4h2 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4
|
2016-02-05 15:55:53 +01:00
|
|
|
spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4
|
2016-02-09 08:58:18 +01:00
|
|
|
spec5h1 spec6b_ii spec6d2 spec6e spec6f
|
2018-02-05 23:38:22 -08:00
|
|
|
spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c spec8d
|
2016-10-17 09:01:00 +01:00
|
|
|
spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g
|
2016-09-16 18:57:40 +02:00
|
|
|
spec10
|
2016-10-24 22:39:59 +02:00
|
|
|
bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509
|
2022-09-05 14:44:50 +01:00
|
|
|
bug512 bug514 bug568 bug633 bug1307 bug1459 bug2276 variable_bug2772
|
2017-02-16 13:59:53 +01:00
|
|
|
name size size2 multiple_feature_blocks omitted_GlyphClassDef
|
2017-02-17 14:19:54 +01:00
|
|
|
ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical
|
2017-02-16 15:06:02 +01:00
|
|
|
ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical
|
2017-02-17 13:49:32 +01:00
|
|
|
ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical
|
2020-07-11 19:56:29 +01:00
|
|
|
PairPosSubtable ChainSubstSubtable SubstSubtable ChainPosSubtable
|
|
|
|
LigatureSubtable AlternateSubtable MultipleSubstSubtable
|
|
|
|
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
|
2020-11-17 15:17:02 +00:00
|
|
|
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
|
2021-02-19 17:17:28 -05:00
|
|
|
GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID
|
2021-10-28 11:58:54 +01:00
|
|
|
variable_scalar_valuerecord variable_scalar_anchor variable_conditionset
|
2023-11-13 16:43:55 +00:00
|
|
|
variable_mark_anchor
|
2016-01-14 08:37:37 +01:00
|
|
|
""".split()
|
|
|
|
|
2021-10-28 11:58:54 +01:00
|
|
|
VARFONT_AXES = [
|
|
|
|
("wght", 200, 200, 1000, "Weight"),
|
|
|
|
("wdth", 100, 100, 200, "Width"),
|
|
|
|
]
|
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
def __init__(self, methodName):
|
|
|
|
unittest.TestCase.__init__(self, methodName)
|
|
|
|
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
|
|
|
|
# and fires deprecation warnings if a program uses the old name.
|
|
|
|
if not hasattr(self, "assertRaisesRegex"):
|
|
|
|
self.assertRaisesRegex = self.assertRaisesRegexp
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
self.tempdir = None
|
|
|
|
self.num_tempfiles = 0
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
if self.tempdir:
|
|
|
|
shutil.rmtree(self.tempdir)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getpath(testfile):
|
|
|
|
path, _ = os.path.split(__file__)
|
2017-01-16 09:36:10 +00:00
|
|
|
return os.path.join(path, "data", testfile)
|
2015-09-04 15:06:11 +02:00
|
|
|
|
|
|
|
def temp_path(self, suffix):
|
|
|
|
if not self.tempdir:
|
|
|
|
self.tempdir = tempfile.mkdtemp()
|
|
|
|
self.num_tempfiles += 1
|
2021-10-28 11:58:54 +01:00
|
|
|
return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix))
|
2015-09-04 15:06:11 +02:00
|
|
|
|
|
|
|
def read_ttx(self, path):
|
|
|
|
lines = []
|
2016-01-26 15:04:18 +00:00
|
|
|
with open(path, "r", encoding="utf-8") as ttx:
|
2015-09-04 15:06:11 +02:00
|
|
|
for line in ttx.readlines():
|
2021-07-30 03:58:20 +02:00
|
|
|
# Elide ttFont attributes because ttLibVersion may change.
|
2015-09-04 15:06:11 +02:00
|
|
|
if line.startswith("<ttFont "):
|
2021-07-30 03:58:20 +02:00
|
|
|
lines.append("<ttFont>\n")
|
2015-09-04 15:06:11 +02:00
|
|
|
else:
|
2021-07-30 03:58:20 +02:00
|
|
|
lines.append(line.rstrip() + "\n")
|
2015-09-04 15:06:11 +02:00
|
|
|
return lines
|
|
|
|
|
2020-09-10 08:29:04 +01:00
|
|
|
def expect_ttx(self, font, expected_ttx, replace=None):
|
2015-09-04 15:06:11 +02:00
|
|
|
path = self.temp_path(suffix=".ttx")
|
2021-10-28 11:58:54 +01:00
|
|
|
font.saveXML(
|
|
|
|
path,
|
|
|
|
tables=[
|
|
|
|
"head",
|
|
|
|
"name",
|
|
|
|
"BASE",
|
|
|
|
"GDEF",
|
|
|
|
"GSUB",
|
|
|
|
"GPOS",
|
|
|
|
"OS/2",
|
|
|
|
"STAT",
|
|
|
|
"hhea",
|
|
|
|
"vhea",
|
|
|
|
],
|
|
|
|
)
|
2015-09-04 15:06:11 +02:00
|
|
|
actual = self.read_ttx(path)
|
|
|
|
expected = self.read_ttx(expected_ttx)
|
2020-09-10 08:29:04 +01:00
|
|
|
if replace:
|
|
|
|
for i in range(len(expected)):
|
2020-09-11 09:35:45 +01:00
|
|
|
for k, v in replace.items():
|
|
|
|
expected[i] = expected[i].replace(k, v)
|
2015-09-04 15:06:11 +02:00
|
|
|
if actual != expected:
|
|
|
|
for line in difflib.unified_diff(
|
2021-10-28 11:58:54 +01:00
|
|
|
expected, actual, fromfile=expected_ttx, tofile=path
|
|
|
|
):
|
2016-12-26 20:29:06 -05:00
|
|
|
sys.stderr.write(line)
|
2015-09-04 15:06:11 +02:00
|
|
|
self.fail("TTX output is different from expected")
|
|
|
|
|
2018-01-24 12:56:24 -08:00
|
|
|
def build(self, featureFile, tables=None):
|
2015-12-04 11:04:37 +01:00
|
|
|
font = makeTTFont()
|
2018-01-24 12:56:24 -08:00
|
|
|
addOpenTypeFeaturesFromString(font, featureFile, tables=tables)
|
2015-09-04 16:11:53 +02:00
|
|
|
return font
|
|
|
|
|
2016-01-14 08:37:37 +01:00
|
|
|
def check_feature_file(self, name):
|
|
|
|
font = makeTTFont()
|
2021-10-28 11:58:54 +01:00
|
|
|
if name.startswith("variable_"):
|
|
|
|
font["name"] = newTable("name")
|
|
|
|
addFvar(font, self.VARFONT_AXES, [])
|
|
|
|
del font["name"]
|
2020-09-10 08:29:04 +01:00
|
|
|
feapath = self.getpath("%s.fea" % name)
|
|
|
|
addOpenTypeFeatures(font, feapath)
|
2016-01-14 08:37:37 +01:00
|
|
|
self.expect_ttx(font, self.getpath("%s.ttx" % name))
|
2021-03-24 08:04:14 -07:00
|
|
|
# Check that:
|
2021-03-22 16:47:44 +00:00
|
|
|
# 1) tables do compile (only G* tables as long as we have a mock font)
|
|
|
|
# 2) dumping after save-reload yields the same TTX dump as before
|
2021-10-28 11:58:54 +01:00
|
|
|
for tag in ("GDEF", "GSUB", "GPOS"):
|
2016-01-26 12:39:41 +01:00
|
|
|
if tag in font:
|
2021-03-22 16:47:44 +00:00
|
|
|
data = font[tag].compile(font)
|
|
|
|
font[tag].decompile(data, font)
|
|
|
|
self.expect_ttx(font, self.getpath("%s.ttx" % name))
|
|
|
|
# Optionally check a debug dump.
|
2020-09-10 08:29:04 +01:00
|
|
|
debugttx = self.getpath("%s-debug.ttx" % name)
|
|
|
|
if os.path.exists(debugttx):
|
2020-09-11 09:35:45 +01:00
|
|
|
addOpenTypeFeatures(font, feapath, debug=True)
|
2021-10-28 11:58:54 +01:00
|
|
|
self.expect_ttx(font, debugttx, replace={"__PATH__": feapath})
|
2016-01-14 08:37:37 +01:00
|
|
|
|
2017-01-06 12:38:14 +00:00
|
|
|
def check_fea2fea_file(self, name, base=None, parser=Parser):
|
2017-02-11 12:05:01 +01:00
|
|
|
font = makeTTFont()
|
2021-10-28 11:58:54 +01:00
|
|
|
fname = (name + ".fea") if "." not in name else name
|
2017-11-16 13:46:27 +00:00
|
|
|
p = parser(self.getpath(fname), glyphNames=font.getGlyphOrder())
|
2017-03-09 14:38:51 +01:00
|
|
|
doc = p.parse()
|
|
|
|
actual = self.normal_fea(doc.asFea().split("\n"))
|
|
|
|
with open(self.getpath(base or fname), "r", encoding="utf-8") as ofile:
|
|
|
|
expected = self.normal_fea(ofile.readlines())
|
|
|
|
|
|
|
|
if expected != actual:
|
|
|
|
fname = name.rsplit(".", 1)[0] + ".fea"
|
|
|
|
for line in difflib.unified_diff(
|
2021-10-28 11:58:54 +01:00
|
|
|
expected,
|
|
|
|
actual,
|
|
|
|
fromfile=fname + " (expected)",
|
|
|
|
tofile=fname + " (actual)",
|
|
|
|
):
|
|
|
|
sys.stderr.write(line + "\n")
|
|
|
|
self.fail(
|
|
|
|
"Fea2Fea output is different from expected. "
|
|
|
|
"Generated:\n{}\n".format("\n".join(actual))
|
|
|
|
)
|
2016-12-20 10:10:29 +00:00
|
|
|
|
2016-12-20 11:03:10 +00:00
|
|
|
def normal_fea(self, lines):
|
2016-12-20 10:10:29 +00:00
|
|
|
output = []
|
|
|
|
skip = 0
|
|
|
|
for l in lines:
|
|
|
|
l = l.strip()
|
2016-12-20 10:24:20 +00:00
|
|
|
if l.startswith("#test-fea2fea:"):
|
|
|
|
if len(l) > 15:
|
|
|
|
output.append(l[15:].strip())
|
2016-12-20 10:10:29 +00:00
|
|
|
skip = 1
|
|
|
|
x = l.find("#")
|
2016-12-20 11:03:10 +00:00
|
|
|
if x >= 0:
|
2016-12-20 10:10:29 +00:00
|
|
|
l = l[:x].strip()
|
2016-12-20 11:03:10 +00:00
|
|
|
if not len(l):
|
|
|
|
continue
|
|
|
|
if skip > 0:
|
2016-12-20 10:10:29 +00:00
|
|
|
skip = skip - 1
|
|
|
|
continue
|
|
|
|
output.append(l)
|
|
|
|
return output
|
|
|
|
|
2023-05-27 03:04:49 +03:00
|
|
|
def make_mock_vf(self):
|
|
|
|
font = makeTTFont()
|
|
|
|
font["name"] = newTable("name")
|
|
|
|
addFvar(font, self.VARFONT_AXES, [])
|
|
|
|
del font["name"]
|
|
|
|
return font
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_region(var_region_axis):
|
|
|
|
return (
|
|
|
|
var_region_axis.StartCoord,
|
|
|
|
var_region_axis.PeakCoord,
|
|
|
|
var_region_axis.EndCoord,
|
|
|
|
)
|
|
|
|
|
2015-09-07 11:14:03 +02:00
|
|
|
def test_alternateSubst_multipleSubstitutionsForSameGlyph(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'Already defined alternates for glyph "A"',
|
2015-09-07 11:14:03 +02:00
|
|
|
self.build,
|
|
|
|
"feature test {"
|
2015-09-08 10:33:07 +02:00
|
|
|
" sub A from [A.alt1 A.alt2];"
|
|
|
|
" sub B from [B.alt1 B.alt2 B.alt3];"
|
|
|
|
" sub A from [A.alt1 A.alt2];"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;",
|
|
|
|
)
|
2015-09-08 10:33:07 +02:00
|
|
|
|
2019-12-18 11:09:23 +01:00
|
|
|
def test_singleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
|
|
|
|
logger = logging.getLogger("fontTools.feaLib.builder")
|
|
|
|
with CapturingLogHandler(logger, "INFO") as captor:
|
|
|
|
self.build(
|
|
|
|
"feature test {"
|
|
|
|
" sub A by A.sc;"
|
|
|
|
" sub B by B.sc;"
|
|
|
|
" sub A by A.sc;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;"
|
|
|
|
)
|
|
|
|
captor.assertRegex(
|
|
|
|
'Removing duplicate single substitution from glyph "A" to "A.sc"'
|
|
|
|
)
|
2019-12-18 11:09:23 +01:00
|
|
|
|
2015-09-10 15:28:02 +02:00
|
|
|
def test_multipleSubst_multipleSubstitutionsForSameGlyph(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'Already defined substitution for glyph "f_f_i"',
|
2015-09-10 15:28:02 +02:00
|
|
|
self.build,
|
|
|
|
"feature test {"
|
|
|
|
" sub f_f_i by f f i;"
|
|
|
|
" sub c_t by c t;"
|
2020-01-26 10:25:52 -05:00
|
|
|
" sub f_f_i by f_f i;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;",
|
|
|
|
)
|
2015-09-10 15:28:02 +02:00
|
|
|
|
2020-01-26 10:25:52 -05:00
|
|
|
def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
|
|
|
|
logger = logging.getLogger("fontTools.feaLib.builder")
|
|
|
|
with CapturingLogHandler(logger, "INFO") as captor:
|
|
|
|
self.build(
|
|
|
|
"feature test {"
|
|
|
|
" sub f_f_i by f f i;"
|
|
|
|
" sub c_t by c t;"
|
|
|
|
" sub f_f_i by f f i;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;"
|
|
|
|
)
|
|
|
|
captor.assertRegex(
|
|
|
|
r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)"
|
|
|
|
)
|
2020-01-26 10:25:52 -05:00
|
|
|
|
2018-01-15 18:45:42 +00:00
|
|
|
def test_pairPos_redefinition_warning(self):
|
|
|
|
# https://github.com/fonttools/fonttools/issues/1147
|
2020-07-02 14:09:10 +01:00
|
|
|
logger = logging.getLogger("fontTools.otlLib.builder")
|
2018-09-15 10:09:49 +02:00
|
|
|
with CapturingLogHandler(logger, "DEBUG") as captor:
|
2018-01-15 18:45:42 +00:00
|
|
|
# the pair "yacute semicolon" is redefined in the enum pos
|
|
|
|
font = self.build(
|
|
|
|
"@Y_LC = [y yacute ydieresis];"
|
|
|
|
"@SMALL_PUNC = [comma semicolon period];"
|
|
|
|
"feature kern {"
|
|
|
|
" pos yacute semicolon -70;"
|
|
|
|
" enum pos @Y_LC semicolon -80;"
|
|
|
|
" pos @Y_LC @SMALL_PUNC -100;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} kern;"
|
|
|
|
)
|
2018-01-15 18:45:42 +00:00
|
|
|
|
|
|
|
captor.assertRegex("Already defined position for pair yacute semicolon")
|
|
|
|
|
|
|
|
# the first definition prevails: yacute semicolon -70
|
|
|
|
st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0]
|
|
|
|
self.assertEqual(st.Coverage.glyphs[2], "yacute")
|
2021-10-28 11:58:54 +01:00
|
|
|
self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, "semicolon")
|
|
|
|
self.assertEqual(
|
|
|
|
vars(st.PairSet[2].PairValueRecord[0].Value1), {"XAdvance": -70}
|
|
|
|
)
|
2015-12-07 21:26:58 +01:00
|
|
|
|
2015-09-08 10:33:07 +02:00
|
|
|
def test_singleSubst_multipleSubstitutionsForSameGlyph(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
'Already defined rule for replacing glyph "e" by "E.sc"',
|
|
|
|
self.build,
|
|
|
|
"feature test {"
|
|
|
|
" sub [a-z] by [A.sc-Z.sc];"
|
|
|
|
" sub e by e.fina;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;",
|
|
|
|
)
|
2015-09-07 11:14:03 +02:00
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
def test_singlePos_redefinition(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'Already defined different position for glyph "A"',
|
|
|
|
self.build,
|
|
|
|
"feature test { pos A 123; pos A 456; } test;",
|
|
|
|
)
|
2015-12-04 11:16:43 +01:00
|
|
|
|
2016-01-11 16:00:52 +01:00
|
|
|
def test_feature_outside_aalt(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
'Feature references are only allowed inside "feature aalt"',
|
2021-10-28 11:58:54 +01:00
|
|
|
self.build,
|
|
|
|
"feature test { feature test; } test;",
|
|
|
|
)
|
2016-01-11 16:00:52 +01:00
|
|
|
|
|
|
|
def test_feature_undefinedReference(self):
|
2023-05-17 15:08:08 +01:00
|
|
|
with warnings.catch_warnings(record=True) as w:
|
|
|
|
self.build("feature aalt { feature none; } aalt;")
|
|
|
|
assert len(w) == 1
|
|
|
|
assert "Feature none has not been defined" in str(w[0].message)
|
2016-01-11 16:00:52 +01:00
|
|
|
|
2016-01-08 19:06:52 +01:00
|
|
|
def test_GlyphClassDef_conflictingClasses(self):
|
|
|
|
self.assertRaisesRegex(
|
2021-10-28 11:58:54 +01:00
|
|
|
FeatureLibError,
|
|
|
|
"Glyph X was assigned to a different class",
|
2016-01-08 19:06:52 +01:00
|
|
|
self.build,
|
|
|
|
"table GDEF {"
|
|
|
|
" GlyphClassDef [a b], [X], , ;"
|
|
|
|
" GlyphClassDef [a b X], , , ;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} GDEF;",
|
|
|
|
)
|
2016-01-08 19:06:52 +01:00
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def test_languagesystem(self):
|
2016-03-20 14:37:40 +01:00
|
|
|
builder = Builder(makeTTFont(), (None, None))
|
2021-10-28 11:58:54 +01:00
|
|
|
builder.add_language_system(None, "latn", "FRA")
|
|
|
|
builder.add_language_system(None, "cyrl", "RUS")
|
|
|
|
builder.start_feature(location=None, name="test")
|
|
|
|
self.assertEqual(builder.language_systems, {("latn", "FRA"), ("cyrl", "RUS")})
|
2015-09-04 22:29:06 +02:00
|
|
|
|
2015-09-08 10:56:07 +02:00
|
|
|
def test_languagesystem_duplicate(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
'"languagesystem cyrl RUS" has already been specified',
|
2021-10-28 11:58:54 +01:00
|
|
|
self.build,
|
|
|
|
"languagesystem cyrl RUS; languagesystem cyrl RUS;",
|
|
|
|
)
|
2015-09-08 10:56:07 +02:00
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def test_languagesystem_none_specified(self):
|
2016-03-20 14:37:40 +01:00
|
|
|
builder = Builder(makeTTFont(), (None, None))
|
2021-10-28 11:58:54 +01:00
|
|
|
builder.start_feature(location=None, name="test")
|
|
|
|
self.assertEqual(builder.language_systems, {("DFLT", "dflt")})
|
2015-09-04 22:29:06 +02:00
|
|
|
|
2015-09-04 16:11:53 +02:00
|
|
|
def test_languagesystem_DFLT_dflt_not_first(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'If "languagesystem DFLT dflt" is present, '
|
2015-09-04 16:11:53 +02:00
|
|
|
"it must be the first of the languagesystem statements",
|
2021-10-28 11:58:54 +01:00
|
|
|
self.build,
|
|
|
|
"languagesystem latn TRK; languagesystem DFLT dflt;",
|
|
|
|
)
|
2015-09-04 16:11:53 +02:00
|
|
|
|
2018-07-24 16:17:23 +01:00
|
|
|
def test_languagesystem_DFLT_not_preceding(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'languagesystems using the "DFLT" script tag must '
|
2018-07-24 16:17:23 +01:00
|
|
|
"precede all other languagesystems",
|
|
|
|
self.build,
|
|
|
|
"languagesystem DFLT dflt; "
|
|
|
|
"languagesystem latn dflt; "
|
2021-10-28 11:58:54 +01:00
|
|
|
"languagesystem DFLT fooo; ",
|
2018-07-24 16:17:23 +01:00
|
|
|
)
|
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def test_script(self):
|
2016-03-20 14:37:40 +01:00
|
|
|
builder = Builder(makeTTFont(), (None, None))
|
2021-10-28 11:58:54 +01:00
|
|
|
builder.start_feature(location=None, name="test")
|
|
|
|
builder.set_script(location=None, script="cyrl")
|
|
|
|
self.assertEqual(builder.language_systems, {("cyrl", "dflt")})
|
2015-09-04 22:29:06 +02:00
|
|
|
|
2015-09-08 12:18:03 +02:00
|
|
|
def test_script_in_aalt_feature(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'Script statements are not allowed within "feature aalt"',
|
|
|
|
self.build,
|
|
|
|
"feature aalt { script latn; } aalt;",
|
|
|
|
)
|
2015-09-08 12:18:03 +02:00
|
|
|
|
|
|
|
def test_script_in_size_feature(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'Script statements are not allowed within "feature size"',
|
|
|
|
self.build,
|
|
|
|
"feature size { script latn; } size;",
|
|
|
|
)
|
2015-09-08 12:18:03 +02:00
|
|
|
|
2020-04-20 23:09:53 +02:00
|
|
|
def test_script_in_standalone_lookup(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Script statements are not allowed within standalone lookup blocks",
|
2021-10-28 11:58:54 +01:00
|
|
|
self.build,
|
|
|
|
"lookup test { script latn; } test;",
|
|
|
|
)
|
2020-04-20 23:09:53 +02:00
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def test_language(self):
|
2016-03-20 14:37:40 +01:00
|
|
|
builder = Builder(makeTTFont(), (None, None))
|
2021-10-28 11:58:54 +01:00
|
|
|
builder.add_language_system(None, "latn", "FRA ")
|
|
|
|
builder.start_feature(location=None, name="test")
|
|
|
|
builder.set_script(location=None, script="cyrl")
|
|
|
|
builder.set_language(
|
|
|
|
location=None, language="RUS ", include_default=False, required=False
|
|
|
|
)
|
|
|
|
self.assertEqual(builder.language_systems, {("cyrl", "RUS ")})
|
|
|
|
builder.set_language(
|
|
|
|
location=None, language="BGR ", include_default=True, required=False
|
|
|
|
)
|
|
|
|
self.assertEqual(builder.language_systems, {("cyrl", "BGR ")})
|
|
|
|
builder.start_feature(location=None, name="test2")
|
|
|
|
self.assertEqual(builder.language_systems, {("latn", "FRA ")})
|
2015-09-04 22:29:06 +02:00
|
|
|
|
2015-09-08 12:18:03 +02:00
|
|
|
def test_language_in_aalt_feature(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'Language statements are not allowed within "feature aalt"',
|
|
|
|
self.build,
|
|
|
|
"feature aalt { language FRA; } aalt;",
|
|
|
|
)
|
2015-09-08 12:18:03 +02:00
|
|
|
|
|
|
|
def test_language_in_size_feature(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'Language statements are not allowed within "feature size"',
|
|
|
|
self.build,
|
|
|
|
"feature size { language FRA; } size;",
|
|
|
|
)
|
2015-09-08 12:18:03 +02:00
|
|
|
|
2020-04-20 23:09:53 +02:00
|
|
|
def test_language_in_standalone_lookup(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Language statements are not allowed within standalone lookup blocks",
|
2021-10-28 11:58:54 +01:00
|
|
|
self.build,
|
|
|
|
"lookup test { language FRA; } test;",
|
|
|
|
)
|
2020-04-20 23:09:53 +02:00
|
|
|
|
2015-09-08 15:55:54 +02:00
|
|
|
def test_language_required_duplicate(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
r"Language FRA \(script latn\) has already specified "
|
|
|
|
"feature scmp as its required feature",
|
|
|
|
self.build,
|
|
|
|
"feature scmp {"
|
|
|
|
" script latn;"
|
|
|
|
" language FRA required;"
|
|
|
|
" language DEU required;"
|
|
|
|
" substitute [a-z] by [A.sc-Z.sc];"
|
|
|
|
"} scmp;"
|
|
|
|
"feature test {"
|
2016-06-22 11:29:42 -07:00
|
|
|
" script latn;"
|
2015-09-08 15:55:54 +02:00
|
|
|
" language FRA required;"
|
|
|
|
" substitute [a-z] by [A.sc-Z.sc];"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;",
|
|
|
|
)
|
2015-09-08 15:55:54 +02:00
|
|
|
|
2015-09-07 13:33:44 +02:00
|
|
|
def test_lookup_already_defined(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
'Lookup "foo" has already been defined',
|
|
|
|
self.build,
|
|
|
|
"lookup foo {} foo; lookup foo {} foo;",
|
|
|
|
)
|
2015-09-07 13:33:44 +02:00
|
|
|
|
2015-09-07 16:27:12 +02:00
|
|
|
def test_lookup_multiple_flags(self):
|
2016-01-06 17:53:26 +01:00
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Within a named lookup block, all rules must be "
|
|
|
|
"of the same lookup type and flag",
|
|
|
|
self.build,
|
|
|
|
"lookup foo {"
|
|
|
|
" lookupflag 1;"
|
|
|
|
" sub f i by f_i;"
|
|
|
|
" lookupflag 2;"
|
|
|
|
" sub f f i by f_f_i;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} foo;",
|
|
|
|
)
|
2015-09-07 16:27:12 +02:00
|
|
|
|
|
|
|
def test_lookup_multiple_types(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Within a named lookup block, all rules must be "
|
|
|
|
"of the same lookup type and flag",
|
|
|
|
self.build,
|
|
|
|
"lookup foo {"
|
|
|
|
" sub f f i by f_f_i;"
|
|
|
|
" sub A from [A.alt1 A.alt2];"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} foo;",
|
|
|
|
)
|
2015-09-07 16:27:12 +02:00
|
|
|
|
2016-02-03 11:39:50 +01:00
|
|
|
def test_lookup_inside_feature_aalt(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Lookup blocks cannot be placed inside 'aalt' features",
|
2021-10-28 11:58:54 +01:00
|
|
|
self.build,
|
|
|
|
"feature aalt {lookup L {} L;} aalt;",
|
|
|
|
)
|
2016-02-03 11:39:50 +01:00
|
|
|
|
2020-04-27 02:30:10 +02:00
|
|
|
def test_chain_subst_refrences_GPOS_looup(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2020-04-27 18:13:18 +02:00
|
|
|
"Missing index of the specified lookup, might be a positioning lookup",
|
2020-04-27 02:30:10 +02:00
|
|
|
self.build,
|
|
|
|
"lookup dummy { pos a 50; } dummy;"
|
|
|
|
"feature test {"
|
|
|
|
" sub a' lookup dummy b;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;",
|
2020-04-27 02:30:10 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
def test_chain_pos_refrences_GSUB_looup(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2020-04-27 18:13:18 +02:00
|
|
|
"Missing index of the specified lookup, might be a substitution lookup",
|
2020-04-27 02:30:10 +02:00
|
|
|
self.build,
|
|
|
|
"lookup dummy { sub a by A; } dummy;"
|
|
|
|
"feature test {"
|
|
|
|
" pos a' lookup dummy b;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;",
|
2020-04-27 02:30:10 +02:00
|
|
|
)
|
|
|
|
|
2020-08-11 21:21:30 -07:00
|
|
|
def test_STAT_elidedfallbackname_already_defined(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"ElidedFallbackName is already set.",
|
2020-08-11 21:21:30 -07:00
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
" ElidedFallbackNameID 256;"
|
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_elidedfallbackname_set_twice(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"ElidedFallbackName is already set.",
|
2020-08-11 21:21:30 -07:00
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
|
|
|
' ElidedFallbackName { name "Italic"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_elidedfallbacknameID_already_defined(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"ElidedFallbackNameID is already set.",
|
2020-08-11 21:21:30 -07:00
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
|
|
|
" ElidedFallbackNameID 256;"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_elidedfallbacknameID_not_in_name_table(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"ElidedFallbackNameID 256 points to a nameID that does not "
|
2020-08-11 21:21:30 -07:00
|
|
|
'exist in the "name" table',
|
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 257 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
|
|
|
" ElidedFallbackNameID 256;"
|
2021-02-19 17:17:28 -05:00
|
|
|
' DesignAxis opsz 1 { name "Optical Size"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_design_axis_name(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
'Expected "name"',
|
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
|
|
|
' DesignAxis opsz 0 { badtag "Optical Size"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_duplicate_design_axis_name(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
'DesignAxis already defined for tag "opsz".',
|
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
|
|
|
' DesignAxis opsz 0 { name "Optical Size"; };'
|
|
|
|
' DesignAxis opsz 1 { name "Optical Size"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_design_axis_duplicate_order(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"DesignAxis already defined for axis number 0.",
|
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
|
|
|
' DesignAxis opsz 0 { name "Optical Size"; };'
|
|
|
|
' DesignAxis wdth 0 { name "Width"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
" AxisValue {"
|
|
|
|
" location opsz 8;"
|
|
|
|
" location wdth 400;"
|
2020-08-11 21:21:30 -07:00
|
|
|
' name "Caption";'
|
2021-10-28 11:58:54 +01:00
|
|
|
" };"
|
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_undefined_tag(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"DesignAxis not defined for wdth.",
|
2020-08-11 21:21:30 -07:00
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
|
|
|
' DesignAxis opsz 0 { name "Optical Size"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
" AxisValue { "
|
|
|
|
" location wdth 125; "
|
2020-08-11 21:21:30 -07:00
|
|
|
' name "Wide"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
" };"
|
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_axis_value_format4(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"Axis tag wdth already defined.",
|
2020-08-11 21:21:30 -07:00
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
|
|
|
' DesignAxis opsz 0 { name "Optical Size"; };'
|
|
|
|
' DesignAxis wdth 1 { name "Width"; };'
|
|
|
|
' DesignAxis wght 2 { name "Weight"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
" AxisValue { "
|
|
|
|
" location opsz 8; "
|
|
|
|
" location wdth 125; "
|
|
|
|
" location wdth 125; "
|
|
|
|
" location wght 500; "
|
2020-08-11 21:21:30 -07:00
|
|
|
' name "Caption Medium Wide"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
" };"
|
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_duplicate_axis_value_record(self):
|
|
|
|
# Test for Duplicate AxisValueRecords even when the definition order
|
|
|
|
# is different.
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"An AxisValueRecord with these values is already defined.",
|
2020-08-11 21:21:30 -07:00
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; };'
|
|
|
|
' DesignAxis opsz 0 { name "Optical Size"; };'
|
|
|
|
' DesignAxis wdth 1 { name "Width"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
" AxisValue {"
|
|
|
|
" location opsz 8;"
|
|
|
|
" location wdth 400;"
|
2020-08-11 21:21:30 -07:00
|
|
|
' name "Caption";'
|
2021-10-28 11:58:54 +01:00
|
|
|
" };"
|
|
|
|
" AxisValue {"
|
|
|
|
" location wdth 400;"
|
|
|
|
" location opsz 8;"
|
2020-08-11 21:21:30 -07:00
|
|
|
' name "Caption";'
|
2021-10-28 11:58:54 +01:00
|
|
|
" };"
|
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_axis_value_missing_location(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
'Expected "Axis location"',
|
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"};"
|
2020-08-11 21:21:30 -07:00
|
|
|
' DesignAxis opsz 0 { name "Optical Size"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
" AxisValue { "
|
2020-08-11 21:21:30 -07:00
|
|
|
' name "Wide"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
" };"
|
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
|
|
|
def test_STAT_invalid_location_tag(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"Tags cannot be longer than 4 characters",
|
2020-08-11 21:21:30 -07:00
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"table name {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' nameid 256 "Roman"; '
|
2021-10-28 11:58:54 +01:00
|
|
|
"} name;"
|
|
|
|
"table STAT {"
|
2020-08-11 21:21:30 -07:00
|
|
|
' ElidedFallbackName { name "Roman"; '
|
|
|
|
' name 3 1 0x0411 "ローマン"; }; '
|
|
|
|
' DesignAxis width 0 { name "Width"; };'
|
2021-10-28 11:58:54 +01:00
|
|
|
"} STAT;",
|
|
|
|
)
|
2020-08-11 21:21:30 -07:00
|
|
|
|
2017-01-04 16:18:50 +00:00
|
|
|
def test_extensions(self):
|
|
|
|
class ast_BaseClass(ast.MarkClass):
|
|
|
|
def asFea(self, indent=""):
|
|
|
|
return ""
|
|
|
|
|
|
|
|
class ast_BaseClassDefinition(ast.MarkClassDefinition):
|
|
|
|
def asFea(self, indent=""):
|
|
|
|
return ""
|
|
|
|
|
|
|
|
class ast_MarkBasePosStatement(ast.MarkBasePosStatement):
|
|
|
|
def asFea(self, indent=""):
|
2017-01-06 12:38:14 +00:00
|
|
|
if isinstance(self.base, ast.MarkClassName):
|
2017-01-04 16:18:50 +00:00
|
|
|
res = ""
|
2017-01-06 12:38:14 +00:00
|
|
|
for bcd in self.base.markClass.definitions:
|
|
|
|
if res != "":
|
|
|
|
res += "\n{}".format(indent)
|
2021-10-28 11:58:54 +01:00
|
|
|
res += "pos base {} {}".format(
|
|
|
|
bcd.glyphs.asFea(), bcd.anchor.asFea()
|
|
|
|
)
|
2017-01-06 12:38:14 +00:00
|
|
|
for m in self.marks:
|
2017-01-04 16:18:50 +00:00
|
|
|
res += " mark @{}".format(m.name)
|
|
|
|
res += ";"
|
2017-01-06 12:38:14 +00:00
|
|
|
else:
|
2017-01-04 16:18:50 +00:00
|
|
|
res = "pos base {}".format(self.base.asFea())
|
|
|
|
for a, m in self.marks:
|
|
|
|
res += " {} mark @{}".format(a.asFea(), m.name)
|
|
|
|
res += ";"
|
|
|
|
return res
|
|
|
|
|
2017-01-06 12:38:14 +00:00
|
|
|
class testAst(object):
|
2017-01-04 16:18:50 +00:00
|
|
|
MarkBasePosStatement = ast_MarkBasePosStatement
|
2021-10-28 11:58:54 +01:00
|
|
|
|
2017-01-04 16:18:50 +00:00
|
|
|
def __getattr__(self, name):
|
|
|
|
return getattr(ast, name)
|
|
|
|
|
|
|
|
class testParser(Parser):
|
|
|
|
def parse_position_base_(self, enumerated, vertical):
|
|
|
|
location = self.cur_token_location_
|
|
|
|
self.expect_keyword_("base")
|
|
|
|
if enumerated:
|
|
|
|
raise FeatureLibError(
|
|
|
|
'"enumerate" is not allowed with '
|
2021-10-28 11:58:54 +01:00
|
|
|
"mark-to-base attachment positioning",
|
|
|
|
location,
|
|
|
|
)
|
2017-01-04 16:18:50 +00:00
|
|
|
base = self.parse_glyphclass_(accept_glyphname=True)
|
2017-01-06 12:38:14 +00:00
|
|
|
if self.next_token_ == "<":
|
2017-01-04 16:18:50 +00:00
|
|
|
marks = self.parse_anchor_marks_()
|
|
|
|
else:
|
|
|
|
marks = []
|
|
|
|
while self.next_token_ == "mark":
|
|
|
|
self.expect_keyword_("mark")
|
|
|
|
m = self.expect_markClass_reference_()
|
|
|
|
marks.append(m)
|
|
|
|
self.expect_symbol_(";")
|
2021-10-28 11:58:54 +01:00
|
|
|
return self.ast.MarkBasePosStatement(base, marks, location=location)
|
2017-01-04 16:18:50 +00:00
|
|
|
|
|
|
|
def parseBaseClass(self):
|
2021-10-28 11:58:54 +01:00
|
|
|
if not hasattr(self.doc_, "baseClasses"):
|
2017-01-04 16:18:50 +00:00
|
|
|
self.doc_.baseClasses = {}
|
|
|
|
location = self.cur_token_location_
|
|
|
|
glyphs = self.parse_glyphclass_(accept_glyphname=True)
|
|
|
|
anchor = self.parse_anchor_()
|
|
|
|
name = self.expect_class_name_()
|
|
|
|
self.expect_symbol_(";")
|
|
|
|
baseClass = self.doc_.baseClasses.get(name)
|
2017-01-06 12:38:14 +00:00
|
|
|
if baseClass is None:
|
2017-01-04 16:18:50 +00:00
|
|
|
baseClass = ast_BaseClass(name)
|
|
|
|
self.doc_.baseClasses[name] = baseClass
|
|
|
|
self.glyphclasses_.define(name, baseClass)
|
2021-10-28 11:58:54 +01:00
|
|
|
bcdef = ast_BaseClassDefinition(
|
|
|
|
baseClass, anchor, glyphs, location=location
|
|
|
|
)
|
2017-01-04 16:18:50 +00:00
|
|
|
baseClass.addDefinition(bcdef)
|
|
|
|
return bcdef
|
|
|
|
|
2021-10-28 11:58:54 +01:00
|
|
|
extensions = {"baseClass": lambda s: s.parseBaseClass()}
|
2017-01-04 16:18:50 +00:00
|
|
|
ast = testAst()
|
|
|
|
|
2017-01-12 13:21:20 +00:00
|
|
|
self.check_fea2fea_file(
|
2021-10-28 11:58:54 +01:00
|
|
|
"baseClass.feax", base="baseClass.fea", parser=testParser
|
|
|
|
)
|
2017-01-04 16:18:50 +00:00
|
|
|
|
2017-01-12 14:39:21 +00:00
|
|
|
def test_markClass_same_glyph_redefined(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Glyph acute already defined",
|
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"markClass [acute] <anchor 350 0> @TOP_MARKS;" * 2,
|
|
|
|
)
|
2017-01-12 14:39:21 +00:00
|
|
|
|
2017-04-30 22:17:30 +02:00
|
|
|
def test_markClass_same_glyph_multiple_classes(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2021-10-28 11:58:54 +01:00
|
|
|
"Glyph uni0327 cannot be in both @ogonek and @cedilla",
|
2017-04-30 22:17:30 +02:00
|
|
|
self.build,
|
|
|
|
"feature mark {"
|
|
|
|
" markClass [uni0327 uni0328] <anchor 0 0> @ogonek;"
|
|
|
|
" pos base [a] <anchor 399 0> mark @ogonek;"
|
|
|
|
" markClass [uni0327] <anchor 0 0> @cedilla;"
|
|
|
|
" pos base [a] <anchor 244 0> mark @cedilla;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} mark;",
|
|
|
|
)
|
2017-04-30 22:17:30 +02:00
|
|
|
|
2018-01-24 12:56:24 -08:00
|
|
|
def test_build_specific_tables(self):
|
|
|
|
features = "feature liga {sub f i by f_i;} liga;"
|
|
|
|
font = self.build(features)
|
|
|
|
assert "GSUB" in font
|
|
|
|
|
|
|
|
font2 = self.build(features, tables=set())
|
|
|
|
assert "GSUB" not in font2
|
|
|
|
|
2018-01-24 14:39:19 -08:00
|
|
|
def test_build_unsupported_tables(self):
|
2020-05-29 16:44:19 +01:00
|
|
|
self.assertRaises(NotImplementedError, self.build, "", tables={"FOO"})
|
2018-01-24 12:56:24 -08:00
|
|
|
|
2018-01-25 09:53:42 -08:00
|
|
|
def test_build_pre_parsed_ast_featurefile(self):
|
2021-03-29 11:45:58 +02:00
|
|
|
f = StringIO("feature liga {sub f i by f_i;} liga;")
|
2018-01-25 09:53:42 -08:00
|
|
|
tree = Parser(f).parse()
|
|
|
|
font = makeTTFont()
|
|
|
|
addOpenTypeFeatures(font, tree)
|
|
|
|
assert "GSUB" in font
|
|
|
|
|
2018-07-24 17:14:39 +01:00
|
|
|
def test_unsupported_subtable_break(self):
|
2020-07-02 14:09:10 +01:00
|
|
|
logger = logging.getLogger("fontTools.otlLib.builder")
|
2021-10-28 11:58:54 +01:00
|
|
|
with CapturingLogHandler(logger, level="WARNING") as captor:
|
2019-02-26 19:53:49 +02:00
|
|
|
self.build(
|
|
|
|
"feature test {"
|
|
|
|
" pos a 10;"
|
|
|
|
" subtable;"
|
|
|
|
" pos b 10;"
|
|
|
|
"} test;"
|
|
|
|
)
|
2019-06-11 13:14:35 +01:00
|
|
|
|
|
|
|
captor.assertRegex(
|
|
|
|
'<features>:1:32: unsupported "subtable" statement for lookup type'
|
|
|
|
)
|
2018-07-24 17:14:39 +01:00
|
|
|
|
2018-07-26 10:51:37 +01:00
|
|
|
def test_skip_featureNames_if_no_name_table(self):
|
|
|
|
features = (
|
|
|
|
"feature ss01 {"
|
|
|
|
" featureNames {"
|
|
|
|
' name "ignored as we request to skip name table";'
|
|
|
|
" };"
|
|
|
|
" sub A by A.alt1;"
|
|
|
|
"} ss01;"
|
|
|
|
)
|
|
|
|
font = self.build(features, tables=["GSUB"])
|
|
|
|
self.assertIn("GSUB", font)
|
|
|
|
self.assertNotIn("name", font)
|
|
|
|
|
2020-07-02 14:09:10 +01:00
|
|
|
def test_singlePos_multiplePositionsForSameGlyph(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Already defined different position for glyph",
|
|
|
|
self.build,
|
2021-10-28 11:58:54 +01:00
|
|
|
"lookup foo {" " pos A -45; " " pos A 45; " "} foo;",
|
|
|
|
)
|
2020-07-02 14:09:10 +01:00
|
|
|
|
|
|
|
def test_pairPos_enumRuleOverridenBySinglePair_DEBUG(self):
|
|
|
|
logger = logging.getLogger("fontTools.otlLib.builder")
|
|
|
|
with CapturingLogHandler(logger, "DEBUG") as captor:
|
|
|
|
self.build(
|
|
|
|
"feature test {"
|
|
|
|
" enum pos A [V Y] -80;"
|
|
|
|
" pos A V -75;"
|
2021-10-28 11:58:54 +01:00
|
|
|
"} test;"
|
|
|
|
)
|
|
|
|
captor.assertRegex("Already defined position for pair A V at")
|
2020-07-02 14:09:10 +01:00
|
|
|
|
2021-04-26 10:07:36 +01:00
|
|
|
def test_ignore_empty_lookup_block(self):
|
|
|
|
# https://github.com/fonttools/fonttools/pull/2277
|
|
|
|
font = self.build(
|
2021-10-28 11:58:54 +01:00
|
|
|
"lookup EMPTY { ; } EMPTY;" "feature ss01 { lookup EMPTY; } ss01;"
|
2021-04-26 10:07:36 +01:00
|
|
|
)
|
|
|
|
assert "GPOS" not in font
|
|
|
|
assert "GSUB" not in font
|
|
|
|
|
2021-11-18 11:31:49 +00:00
|
|
|
def test_disable_empty_classes(self):
|
|
|
|
for test in [
|
|
|
|
"sub a by c []",
|
|
|
|
"sub f f [] by f",
|
|
|
|
"ignore sub a []'",
|
|
|
|
"ignore sub [] a'",
|
|
|
|
"sub a []' by b",
|
|
|
|
"sub [] a' by b",
|
|
|
|
"rsub [] by a",
|
|
|
|
"pos [] 120",
|
|
|
|
"pos a [] 120",
|
|
|
|
"enum pos a [] 120",
|
|
|
|
"pos cursive [] <anchor NULL> <anchor NULL>",
|
|
|
|
"pos base [] <anchor NULL> mark @TOPMARKS",
|
|
|
|
"pos ligature [] <anchor NULL> mark @TOPMARKS",
|
|
|
|
"pos mark [] <anchor NULL> mark @TOPMARKS",
|
|
|
|
"ignore pos a []'",
|
|
|
|
"ignore pos [] a'",
|
|
|
|
]:
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Empty ",
|
|
|
|
self.build,
|
|
|
|
f"markClass a <anchor 150 -10> @TOPMARKS; lookup foo {{ {test}; }} foo;",
|
|
|
|
)
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Empty glyph class in mark class definition",
|
|
|
|
self.build,
|
|
|
|
"markClass [] <anchor 150 -10> @TOPMARKS;",
|
|
|
|
)
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
'Expected a glyph class with 1 elements after "by", but found a glyph class with 0 elements',
|
|
|
|
self.build,
|
|
|
|
"feature test { sub a by []; test};",
|
|
|
|
)
|
|
|
|
|
2023-01-10 00:03:48 +02:00
|
|
|
def test_unmarked_ignore_statement(self):
|
|
|
|
name = "bug2949"
|
|
|
|
logger = logging.getLogger("fontTools.feaLib.parser")
|
|
|
|
with CapturingLogHandler(logger, level="WARNING") as captor:
|
|
|
|
self.check_feature_file(name)
|
|
|
|
self.check_fea2fea_file(name)
|
|
|
|
|
|
|
|
for line, sub in {(3, "sub"), (8, "pos"), (13, "sub")}:
|
|
|
|
captor.assertRegex(
|
|
|
|
f'{name}.fea:{line}:12: Ambiguous "ignore {sub}", there should be least one marked glyph'
|
|
|
|
)
|
|
|
|
|
2024-01-10 19:06:26 +02:00
|
|
|
def test_conditionset_multiple_features(self):
|
|
|
|
"""Test that using the same `conditionset` for multiple features reuses the
|
|
|
|
`FeatureVariationRecord`."""
|
|
|
|
|
|
|
|
features = """
|
|
|
|
languagesystem DFLT dflt;
|
|
|
|
|
|
|
|
conditionset test {
|
|
|
|
wght 600 1000;
|
|
|
|
wdth 150 200;
|
|
|
|
} test;
|
|
|
|
|
|
|
|
variation ccmp test {
|
|
|
|
sub e by a;
|
|
|
|
} ccmp;
|
|
|
|
|
|
|
|
variation rlig test {
|
|
|
|
sub b by c;
|
|
|
|
} rlig;
|
|
|
|
"""
|
|
|
|
|
|
|
|
def make_mock_vf():
|
|
|
|
font = makeTTFont()
|
|
|
|
font["name"] = newTable("name")
|
|
|
|
addFvar(
|
|
|
|
font,
|
|
|
|
[("wght", 0, 0, 1000, "Weight"), ("wdth", 100, 100, 200, "Width")],
|
|
|
|
[],
|
|
|
|
)
|
|
|
|
del font["name"]
|
|
|
|
return font
|
|
|
|
|
|
|
|
font = make_mock_vf()
|
|
|
|
addOpenTypeFeaturesFromString(font, features)
|
|
|
|
|
|
|
|
table = font["GSUB"].table
|
|
|
|
assert table.FeatureVariations.FeatureVariationCount == 1
|
|
|
|
|
|
|
|
fvr = table.FeatureVariations.FeatureVariationRecord[0]
|
|
|
|
assert fvr.FeatureTableSubstitution.SubstitutionCount == 2
|
|
|
|
|
2023-03-14 18:26:59 +00:00
|
|
|
def test_condition_set_avar(self):
|
|
|
|
"""Test that the `avar` table is consulted when normalizing user-space
|
|
|
|
values."""
|
|
|
|
|
|
|
|
features = """
|
|
|
|
languagesystem DFLT dflt;
|
|
|
|
|
|
|
|
lookup conditional_sub {
|
|
|
|
sub e by a;
|
|
|
|
} conditional_sub;
|
|
|
|
|
|
|
|
conditionset test {
|
|
|
|
wght 600 1000;
|
2023-03-15 16:18:47 +00:00
|
|
|
wdth 150 200;
|
2023-03-14 18:26:59 +00:00
|
|
|
} test;
|
|
|
|
|
|
|
|
variation rlig test {
|
|
|
|
lookup conditional_sub;
|
|
|
|
} rlig;
|
|
|
|
"""
|
|
|
|
|
|
|
|
def make_mock_vf():
|
|
|
|
font = makeTTFont()
|
|
|
|
font["name"] = newTable("name")
|
2023-03-15 16:18:47 +00:00
|
|
|
addFvar(
|
|
|
|
font,
|
|
|
|
[("wght", 0, 0, 1000, "Weight"), ("wdth", 100, 100, 200, "Width")],
|
|
|
|
[],
|
|
|
|
)
|
2023-03-14 18:26:59 +00:00
|
|
|
del font["name"]
|
|
|
|
return font
|
|
|
|
|
|
|
|
# Without `avar`:
|
|
|
|
font = make_mock_vf()
|
|
|
|
addOpenTypeFeaturesFromString(font, features)
|
2023-03-15 16:18:47 +00:00
|
|
|
condition_table = (
|
2023-03-14 18:26:59 +00:00
|
|
|
font.tables["GSUB"]
|
|
|
|
.table.FeatureVariations.FeatureVariationRecord[0]
|
2023-03-15 16:18:47 +00:00
|
|
|
.ConditionSet.ConditionTable
|
2023-03-14 18:26:59 +00:00
|
|
|
)
|
2023-03-15 16:18:47 +00:00
|
|
|
# user-space wdth=150 and wght=600:
|
|
|
|
assert condition_table[0].FilterRangeMinValue == 0.5
|
|
|
|
assert condition_table[1].FilterRangeMinValue == 0.6
|
2023-03-14 18:26:59 +00:00
|
|
|
|
2023-03-15 16:18:47 +00:00
|
|
|
# With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to
|
|
|
|
# the right, but leaving the wdth axis alone:
|
2023-03-14 18:26:59 +00:00
|
|
|
font = make_mock_vf()
|
|
|
|
font["avar"] = newTable("avar")
|
|
|
|
font["avar"].segments = {"wght": {-1.0: -1.0, 0.0: 0.0, 0.5: 0.625, 1.0: 1.0}}
|
|
|
|
addOpenTypeFeaturesFromString(font, features)
|
2023-03-15 16:18:47 +00:00
|
|
|
condition_table = (
|
2023-03-14 18:26:59 +00:00
|
|
|
font.tables["GSUB"]
|
|
|
|
.table.FeatureVariations.FeatureVariationRecord[0]
|
2023-03-15 16:18:47 +00:00
|
|
|
.ConditionSet.ConditionTable
|
2023-03-14 18:26:59 +00:00
|
|
|
)
|
2023-03-15 16:18:47 +00:00
|
|
|
# user-space wdth=150 as before and wght=600 shifted to the right:
|
|
|
|
assert condition_table[0].FilterRangeMinValue == 0.5
|
|
|
|
assert condition_table[1].FilterRangeMinValue == 0.7
|
2023-03-14 18:26:59 +00:00
|
|
|
|
2023-03-15 16:19:39 +00:00
|
|
|
def test_variable_scalar_avar(self):
|
|
|
|
"""Test that the `avar` table is consulted when normalizing user-space
|
|
|
|
values."""
|
|
|
|
|
|
|
|
features = """
|
|
|
|
languagesystem DFLT dflt;
|
|
|
|
|
|
|
|
feature kern {
|
|
|
|
pos cursive one <anchor 0 (wght=200:12 wght=900:22 wdth=150,wght=900:42)> <anchor NULL>;
|
|
|
|
pos two <0 (wght=200:12 wght=900:22 wdth=150,wght=900:42) 0 0>;
|
|
|
|
} kern;
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Without `avar` (wght=200, wdth=100 is the default location):
|
2023-05-27 03:04:49 +03:00
|
|
|
font = self.make_mock_vf()
|
2023-03-15 16:19:39 +00:00
|
|
|
addOpenTypeFeaturesFromString(font, features)
|
|
|
|
|
|
|
|
var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
|
|
|
|
var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
|
|
|
|
var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1]
|
2023-05-27 03:04:49 +03:00
|
|
|
assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875)
|
|
|
|
assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
|
2023-03-15 16:19:39 +00:00
|
|
|
var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
|
|
|
|
var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1]
|
2023-05-27 03:04:49 +03:00
|
|
|
assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875)
|
|
|
|
assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5)
|
2023-03-15 16:19:39 +00:00
|
|
|
|
|
|
|
# With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to
|
|
|
|
# the right, but leaving the wdth axis alone:
|
2023-05-27 03:04:49 +03:00
|
|
|
font = self.make_mock_vf()
|
2023-03-15 16:19:39 +00:00
|
|
|
font["avar"] = newTable("avar")
|
|
|
|
font["avar"].segments = {"wght": {-1.0: -1.0, 0.0: 0.0, 0.5: 0.625, 1.0: 1.0}}
|
|
|
|
addOpenTypeFeaturesFromString(font, features)
|
|
|
|
|
|
|
|
var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
|
|
|
|
var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
|
|
|
|
var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1]
|
2023-05-27 03:04:49 +03:00
|
|
|
assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625)
|
|
|
|
assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
|
2023-03-15 16:19:39 +00:00
|
|
|
var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
|
|
|
|
var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1]
|
2023-05-27 03:04:49 +03:00
|
|
|
assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625)
|
|
|
|
assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5)
|
|
|
|
|
|
|
|
def test_ligatureCaretByPos_variable_scalar(self):
|
|
|
|
"""Test that the `avar` table is consulted when normalizing user-space
|
|
|
|
values."""
|
|
|
|
|
|
|
|
features = """
|
|
|
|
table GDEF {
|
|
|
|
LigatureCaretByPos f_i (wght=200:400 wght=900:1000) 380;
|
|
|
|
} GDEF;
|
|
|
|
"""
|
|
|
|
|
|
|
|
font = self.make_mock_vf()
|
|
|
|
addOpenTypeFeaturesFromString(font, features)
|
|
|
|
|
|
|
|
table = font["GDEF"].table
|
|
|
|
lig_glyph = table.LigCaretList.LigGlyph[0]
|
|
|
|
assert lig_glyph.CaretValue[0].Format == 1
|
|
|
|
assert lig_glyph.CaretValue[0].Coordinate == 380
|
|
|
|
assert lig_glyph.CaretValue[1].Format == 3
|
|
|
|
assert lig_glyph.CaretValue[1].Coordinate == 400
|
|
|
|
|
|
|
|
var_region_list = table.VarStore.VarRegionList
|
|
|
|
var_region_axis = var_region_list.Region[0].VarRegionAxis[0]
|
|
|
|
assert self.get_region(var_region_axis) == (0.0, 0.875, 0.875)
|
2023-03-15 16:19:39 +00:00
|
|
|
|
2021-11-18 11:31:49 +00:00
|
|
|
|
2016-01-14 09:43:42 +00:00
|
|
|
def generate_feature_file_test(name):
|
|
|
|
return lambda self: self.check_feature_file(name)
|
|
|
|
|
2016-12-20 11:03:10 +00:00
|
|
|
|
2016-01-14 09:43:42 +00:00
|
|
|
for name in BuilderTest.TEST_FEATURE_FILES:
|
2021-10-28 11:58:54 +01:00
|
|
|
setattr(BuilderTest, "test_FeatureFile_%s" % name, generate_feature_file_test(name))
|
2016-01-14 09:43:42 +00:00
|
|
|
|
2016-12-20 11:03:10 +00:00
|
|
|
|
2016-12-20 10:10:29 +00:00
|
|
|
def generate_fea2fea_file_test(name):
|
|
|
|
return lambda self: self.check_fea2fea_file(name)
|
|
|
|
|
2016-12-20 11:03:10 +00:00
|
|
|
|
2016-12-20 11:06:32 +00:00
|
|
|
for name in BuilderTest.TEST_FEATURE_FILES:
|
2021-10-28 11:58:54 +01:00
|
|
|
setattr(
|
|
|
|
BuilderTest,
|
|
|
|
"test_Fea2feaFile_{}".format(name),
|
|
|
|
generate_fea2fea_file_test(name),
|
|
|
|
)
|
2016-12-20 10:10:29 +00:00
|
|
|
|
2016-12-20 11:03:10 +00:00
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
if __name__ == "__main__":
|
2017-01-11 13:05:35 +00:00
|
|
|
sys.exit(unittest.main())
|