2015-09-04 15:06:11 +02:00
|
|
|
from __future__ import print_function, division, absolute_import
|
|
|
|
from __future__ import unicode_literals
|
2015-09-04 22:29:06 +02:00
|
|
|
from fontTools.feaLib.builder import Builder, addOpenTypeFeatures
|
2015-12-22 14:42:13 +01:00
|
|
|
from fontTools.feaLib.builder import ClassDefBuilder, LigatureSubstBuilder
|
2015-09-04 16:11:53 +02:00
|
|
|
from fontTools.feaLib.error import FeatureLibError
|
2015-09-04 15:06:11 +02:00
|
|
|
from fontTools.ttLib import TTFont
|
2015-12-22 14:42:13 +01:00
|
|
|
from fontTools.ttLib.tables import otTables
|
2015-09-04 15:06:11 +02:00
|
|
|
import codecs
|
|
|
|
import difflib
|
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
import sys
|
|
|
|
import tempfile
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
2015-12-04 11:04:37 +01:00
|
|
|
def makeTTFont():
|
|
|
|
glyphs = (
|
2015-12-21 15:16:47 +01:00
|
|
|
".notdef space slash fraction semicolon period comma "
|
2015-12-04 11:04:37 +01:00
|
|
|
"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 onehalf "
|
|
|
|
"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 "
|
|
|
|
"d.alt n.end s.end "
|
2015-12-09 16:51:15 +01:00
|
|
|
"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 "
|
2015-12-21 15:16:47 +01:00
|
|
|
"ydieresis yacute "
|
2015-12-10 19:17:11 +01:00
|
|
|
"grave acute dieresis macron circumflex cedilla umlaut ogonek caron "
|
2015-12-21 15:16:47 +01:00
|
|
|
"damma hamza sukun kasratan lam_meem_jeem "
|
2015-12-04 11:04:37 +01:00
|
|
|
).split()
|
|
|
|
font = TTFont()
|
|
|
|
font.setGlyphOrder(glyphs)
|
|
|
|
return font
|
|
|
|
|
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
class BuilderTest(unittest.TestCase):
|
|
|
|
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__)
|
|
|
|
return os.path.join(path, "testdata", testfile)
|
|
|
|
|
|
|
|
def temp_path(self, suffix):
|
|
|
|
if not self.tempdir:
|
|
|
|
self.tempdir = tempfile.mkdtemp()
|
|
|
|
self.num_tempfiles += 1
|
|
|
|
return os.path.join(self.tempdir,
|
|
|
|
"tmp%d%s" % (self.num_tempfiles, suffix))
|
|
|
|
|
|
|
|
def read_ttx(self, path):
|
|
|
|
lines = []
|
|
|
|
with codecs.open(path, "r", "utf-8") as ttx:
|
|
|
|
for line in ttx.readlines():
|
|
|
|
# Elide ttFont attributes because ttLibVersion may change,
|
|
|
|
# and use os-native line separators so we can run difflib.
|
|
|
|
if line.startswith("<ttFont "):
|
|
|
|
lines.append("<ttFont>" + os.linesep)
|
|
|
|
else:
|
|
|
|
lines.append(line.rstrip() + os.linesep)
|
|
|
|
return lines
|
|
|
|
|
|
|
|
def expect_ttx(self, font, expected_ttx):
|
|
|
|
path = self.temp_path(suffix=".ttx")
|
2015-12-08 17:04:21 +01:00
|
|
|
font.saveXML(path, quiet=True, tables=['GDEF', 'GSUB', 'GPOS'])
|
2015-09-04 15:06:11 +02:00
|
|
|
actual = self.read_ttx(path)
|
|
|
|
expected = self.read_ttx(expected_ttx)
|
|
|
|
if actual != expected:
|
|
|
|
for line in difflib.unified_diff(
|
|
|
|
expected, actual, fromfile=path, tofile=expected_ttx):
|
|
|
|
sys.stdout.write(line)
|
|
|
|
self.fail("TTX output is different from expected")
|
|
|
|
|
2015-09-04 16:11:53 +02:00
|
|
|
def build(self, featureFile):
|
|
|
|
path = self.temp_path(suffix=".fea")
|
|
|
|
with codecs.open(path, "wb", "utf-8") as outfile:
|
|
|
|
outfile.write(featureFile)
|
2015-12-04 11:04:37 +01:00
|
|
|
font = makeTTFont()
|
2015-09-04 16:11:53 +02:00
|
|
|
addOpenTypeFeatures(path, font)
|
|
|
|
return font
|
|
|
|
|
2015-09-07 11:14:03 +02:00
|
|
|
def test_alternateSubst_multipleSubstitutionsForSameGlyph(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Already defined alternates for glyph \"A\"",
|
|
|
|
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];"
|
|
|
|
"} test;")
|
|
|
|
|
2015-09-10 15:28:02 +02:00
|
|
|
def test_multipleSubst_multipleSubstitutionsForSameGlyph(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Already defined substitution for glyph \"f_f_i\"",
|
|
|
|
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;"
|
|
|
|
"} test;")
|
|
|
|
|
2015-12-07 21:26:58 +01:00
|
|
|
def test_pairPos_redefinition(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2015-12-21 16:06:59 +01:00
|
|
|
r"Already defined position for pair A B "
|
|
|
|
"at .*:2:[0-9]+", # :2: = line 2
|
2015-12-07 21:26:58 +01:00
|
|
|
self.build,
|
|
|
|
"feature test {\n"
|
2015-12-21 16:06:59 +01:00
|
|
|
" pos A B 123;\n" # line 2
|
|
|
|
" pos A B 456;\n"
|
2015-12-07 21:26:58 +01:00
|
|
|
"} test;\n")
|
|
|
|
|
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;"
|
2015-09-07 11:14:03 +02:00
|
|
|
"} test;")
|
|
|
|
|
2015-12-04 11:16:43 +01:00
|
|
|
def test_singlePos_redefinition(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Already defined different position for glyph \"A\"",
|
|
|
|
self.build, "feature test { pos A 123; pos A 456; } test;")
|
|
|
|
|
2015-12-21 15:16:47 +01:00
|
|
|
def test_constructs(self):
|
|
|
|
for name in ("enum markClass language_required "
|
|
|
|
"lookup lookupflag").split():
|
|
|
|
font = makeTTFont()
|
|
|
|
addOpenTypeFeatures(self.getpath("%s.fea" % name), font)
|
|
|
|
self.expect_ttx(font, self.getpath("%s.ttx" % name))
|
|
|
|
|
2015-12-07 23:56:08 +01:00
|
|
|
def test_GPOS(self):
|
2015-12-23 11:35:49 +01:00
|
|
|
for name in "1 2 2b 3 4 5 6 8".split():
|
2015-12-07 23:56:08 +01:00
|
|
|
font = makeTTFont()
|
|
|
|
addOpenTypeFeatures(self.getpath("GPOS_%s.fea" % name), font)
|
|
|
|
self.expect_ttx(font, self.getpath("GPOS_%s.ttx" % name))
|
2015-12-07 21:26:58 +01:00
|
|
|
|
2016-01-06 17:33:34 +01:00
|
|
|
def test_GSUB(self):
|
|
|
|
for name in "2 3 6 8".split():
|
|
|
|
font = makeTTFont()
|
|
|
|
addOpenTypeFeatures(self.getpath("GSUB_%s.fea" % name), font)
|
|
|
|
self.expect_ttx(font, self.getpath("GSUB_%s.ttx" % name))
|
|
|
|
|
2015-12-08 17:19:30 +01:00
|
|
|
def test_spec(self):
|
2016-01-06 16:15:26 +01:00
|
|
|
for name in "4h1 5d1 5d2 5fi1 5fi2 5h1 6d2 6e 6f 6h_ii".split():
|
2015-12-08 17:19:30 +01:00
|
|
|
font = makeTTFont()
|
|
|
|
addOpenTypeFeatures(self.getpath("spec%s.fea" % name), font)
|
|
|
|
self.expect_ttx(font, self.getpath("spec%s.ttx" % name))
|
2015-12-03 13:05:42 +01:00
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def test_languagesystem(self):
|
2015-12-04 11:04:37 +01:00
|
|
|
builder = Builder(None, makeTTFont())
|
2015-09-04 22:29:06 +02: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-08 10:56:07 +02:00
|
|
|
def test_languagesystem_duplicate(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
'"languagesystem cyrl RUS" has already been specified',
|
|
|
|
self.build, "languagesystem cyrl RUS; languagesystem cyrl RUS;")
|
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def test_languagesystem_none_specified(self):
|
2015-12-04 11:04:37 +01:00
|
|
|
builder = Builder(None, makeTTFont())
|
2015-09-04 22:29:06 +02:00
|
|
|
builder.start_feature(location=None, name='test')
|
|
|
|
self.assertEqual(builder.language_systems, {('DFLT', 'dflt')})
|
|
|
|
|
2015-09-04 16:11:53 +02:00
|
|
|
def test_languagesystem_DFLT_dflt_not_first(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"If \"languagesystem DFLT dflt\" is present, "
|
|
|
|
"it must be the first of the languagesystem statements",
|
|
|
|
self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
|
|
|
|
|
2015-12-08 17:04:21 +01:00
|
|
|
def test_markClass_redefine(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
2015-12-12 12:54:23 +01:00
|
|
|
"Glyph C cannot be both in markClass @MARK1 and @MARK2",
|
2015-12-08 17:04:21 +01:00
|
|
|
self.build,
|
|
|
|
"markClass [A B C] <anchor 100 50> @MARK1;"
|
|
|
|
"markClass [C D E] <anchor 200 80> @MARK2;")
|
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def test_script(self):
|
2015-12-04 11:04:37 +01:00
|
|
|
builder = Builder(None, makeTTFont())
|
2015-09-04 22:29:06 +02:00
|
|
|
builder.start_feature(location=None, name='test')
|
|
|
|
builder.set_script(location=None, script='cyrl')
|
|
|
|
self.assertEqual(builder.language_systems,
|
|
|
|
{('DFLT', 'dflt'), ('cyrl', 'dflt')})
|
|
|
|
|
2015-09-08 12:18:03 +02:00
|
|
|
def test_script_in_aalt_feature(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Script statements are not allowed within \"feature aalt\"",
|
|
|
|
self.build, "feature aalt { script latn; } aalt;")
|
|
|
|
|
2015-09-07 13:33:44 +02:00
|
|
|
def test_script_in_lookup_block(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Within a named lookup block, it is not allowed "
|
|
|
|
"to change the script",
|
|
|
|
self.build, "lookup Foo { script latn; } Foo;")
|
|
|
|
|
2015-09-08 12:18:03 +02:00
|
|
|
def test_script_in_size_feature(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Script statements are not allowed within \"feature size\"",
|
|
|
|
self.build, "feature size { script latn; } size;")
|
|
|
|
|
2015-09-04 22:29:06 +02:00
|
|
|
def test_language(self):
|
2015-12-04 11:04:37 +01:00
|
|
|
builder = Builder(None, makeTTFont())
|
2015-09-07 21:34:10 +02:00
|
|
|
builder.add_language_system(None, 'latn', 'FRA ')
|
2015-09-04 22:29:06 +02:00
|
|
|
builder.start_feature(location=None, name='test')
|
|
|
|
builder.set_script(location=None, script='cyrl')
|
2015-09-07 21:34:10 +02:00
|
|
|
builder.set_language(location=None, language='RUS ',
|
2015-09-08 15:55:54 +02:00
|
|
|
include_default=False, required=False)
|
2015-09-07 21:34:10 +02:00
|
|
|
self.assertEqual(builder.language_systems, {('cyrl', 'RUS ')})
|
|
|
|
builder.set_language(location=None, language='BGR ',
|
2015-09-08 15:55:54 +02:00
|
|
|
include_default=True, required=False)
|
2015-09-04 22:29:06 +02:00
|
|
|
self.assertEqual(builder.language_systems,
|
2015-09-07 21:34:10 +02:00
|
|
|
{('latn', 'FRA '), ('cyrl', 'BGR ')})
|
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,
|
|
|
|
"Language statements are not allowed within \"feature aalt\"",
|
|
|
|
self.build, "feature aalt { language FRA; } aalt;")
|
|
|
|
|
2015-09-07 13:33:44 +02:00
|
|
|
def test_language_in_lookup_block(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Within a named lookup block, it is not allowed "
|
|
|
|
"to change the language",
|
|
|
|
self.build, "lookup Foo { language RUS; } Foo;")
|
|
|
|
|
2015-09-08 12:18:03 +02:00
|
|
|
def test_language_in_size_feature(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Language statements are not allowed within \"feature size\"",
|
|
|
|
self.build, "feature size { language FRA; } size;")
|
|
|
|
|
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 {"
|
|
|
|
" language FRA required;"
|
|
|
|
" substitute [a-z] by [A.sc-Z.sc];"
|
|
|
|
"} test;")
|
|
|
|
|
2015-09-07 13:33:44 +02:00
|
|
|
def test_lookup_already_defined(self):
|
|
|
|
self.assertRaisesRegex(
|
|
|
|
FeatureLibError,
|
|
|
|
"Lookup \"foo\" has already been defined",
|
|
|
|
self.build, "lookup foo {} foo; lookup foo {} foo;")
|
|
|
|
|
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;"
|
|
|
|
"} 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];"
|
|
|
|
"} foo;")
|
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
|
2015-12-22 14:42:13 +01:00
|
|
|
class ClassDefBuilderTest(unittest.TestCase):
|
2015-12-23 15:14:00 +01:00
|
|
|
def test_build_ClassDef1(self):
|
|
|
|
builder = ClassDefBuilder(otTables.ClassDef1)
|
2015-12-22 14:42:13 +01:00
|
|
|
builder.add({"a", "b"})
|
|
|
|
builder.add({"c"})
|
|
|
|
builder.add({"e", "f", "g", "h"})
|
2015-12-23 15:14:00 +01:00
|
|
|
cdef = builder.build()
|
|
|
|
self.assertIsInstance(cdef, otTables.ClassDef1)
|
2015-12-22 14:42:13 +01:00
|
|
|
self.assertEqual(cdef.classDefs, {
|
|
|
|
"a": 1,
|
|
|
|
"b": 1,
|
|
|
|
"c": 2
|
|
|
|
})
|
|
|
|
|
2015-12-23 15:14:00 +01:00
|
|
|
def test_build_ClassDef2(self):
|
|
|
|
builder = ClassDefBuilder(otTables.ClassDef2)
|
|
|
|
builder.add({"a", "b"})
|
|
|
|
builder.add({"c"})
|
|
|
|
builder.add({"e", "f", "g", "h"})
|
|
|
|
cdef = builder.build()
|
2015-12-23 11:35:49 +01:00
|
|
|
self.assertIsInstance(cdef, otTables.ClassDef2)
|
|
|
|
self.assertEqual(cdef.classDefs, {
|
2015-12-23 15:14:00 +01:00
|
|
|
"a": 2,
|
|
|
|
"b": 2,
|
|
|
|
"c": 3,
|
|
|
|
"e": 1,
|
|
|
|
"f": 1,
|
|
|
|
"g": 1,
|
|
|
|
"h": 1
|
2015-12-23 11:35:49 +01:00
|
|
|
})
|
|
|
|
|
2015-12-22 14:42:13 +01:00
|
|
|
def test_canAdd(self):
|
|
|
|
b = ClassDefBuilder(otTables.ClassDef1)
|
|
|
|
b.add({"a", "b", "c", "d"})
|
|
|
|
b.add({"e", "f"})
|
|
|
|
self.assertTrue(b.canAdd({"a", "b", "c", "d"}))
|
|
|
|
self.assertTrue(b.canAdd({"e", "f"}))
|
|
|
|
self.assertTrue(b.canAdd({"g", "h", "i"}))
|
|
|
|
self.assertFalse(b.canAdd({"b", "c", "d"}))
|
|
|
|
self.assertFalse(b.canAdd({"a", "b", "c", "d", "e", "f"}))
|
|
|
|
self.assertFalse(b.canAdd({"d", "e", "f"}))
|
|
|
|
self.assertFalse(b.canAdd({"f"}))
|
|
|
|
|
|
|
|
|
2015-09-07 16:10:13 +02:00
|
|
|
class LigatureSubstBuilderTest(unittest.TestCase):
|
|
|
|
def test_make_key(self):
|
2015-12-22 14:42:13 +01:00
|
|
|
self.assertEqual(LigatureSubstBuilder.make_key(("f", "f", "i")),
|
|
|
|
(-3, ("f", "f", "i")))
|
2015-09-07 16:10:13 +02:00
|
|
|
|
|
|
|
|
2015-09-04 15:06:11 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
unittest.main()
|