aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2019-02-01 17:02:23 -0800
committerHaibo Huang <hhb@google.com>2019-02-02 08:08:41 +0000
commitf08648c1c47988dac15b482a048685bf968f8216 (patch)
tree66627cf2059915c54933d5e5a9ed95a8817d8086
parent2995b148b5b97eec8eeed2f0ca97f513c5860674 (diff)
downloadfonttools-f08648c1c47988dac15b482a048685bf968f8216.tar.gz
Upgrade fonttools to 3.37.0
Test: build Change-Id: I8017400b21417deaefbe448e0880b06b66aefe8a
-rwxr-xr-x[-rw-r--r--].travis/after_success.sh0
-rwxr-xr-x[-rw-r--r--].travis/before_install.sh0
-rwxr-xr-x[-rw-r--r--].travis/install.sh0
-rwxr-xr-x[-rw-r--r--].travis/run.sh0
-rw-r--r--Doc/source/designspaceLib/readme.rst5
-rw-r--r--Doc/source/designspaceLib/scripting.rst2
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/designspaceLib/__init__.py12
-rw-r--r--Lib/fontTools/feaLib/ast.py45
-rw-r--r--Lib/fontTools/feaLib/builder.py2
-rw-r--r--Lib/fontTools/feaLib/parser.py2
-rw-r--r--Lib/fontTools/fontBuilder.py24
-rw-r--r--Lib/fontTools/misc/psCharStrings.py28
-rw-r--r--Lib/fontTools/misc/py23.py29
-rw-r--r--Lib/fontTools/subset/__init__.py58
-rw-r--r--Lib/fontTools/subset/cff.py100
-rw-r--r--Lib/fontTools/svgLib/path/arc.py157
-rw-r--r--Lib/fontTools/svgLib/path/parser.py44
-rw-r--r--Lib/fontTools/ttLib/tables/_c_m_a_p.py78
-rw-r--r--Lib/fontTools/ttLib/tables/_h_m_t_x.py39
-rw-r--r--Lib/fontTools/ttLib/tables/_m_a_x_p.py1
-rwxr-xr-x[-rw-r--r--]Lib/fontTools/ttLib/tables/otData.py0
-rw-r--r--Lib/fontTools/ttx.py11
-rwxr-xr-x[-rw-r--r--]Lib/fontTools/ufoLib/__init__.py16
-rwxr-xr-x[-rw-r--r--]Lib/fontTools/ufoLib/glifLib.py8
-rw-r--r--Lib/fontTools/varLib/__init__.py89
-rw-r--r--Lib/fontTools/varLib/mutator.py10
-rw-r--r--Lib/fontTools/voltLib/ast.py28
-rw-r--r--Lib/fontTools/voltLib/parser.py55
-rw-r--r--Lib/fonttools.egg-info/PKG-INFO64
-rw-r--r--Lib/fonttools.egg-info/SOURCES.txt11
-rw-r--r--Lib/fonttools.egg-info/requires.txt4
-rw-r--r--METADATA8
-rwxr-xr-x[-rw-r--r--]MetaTools/buildTableList.py0
-rwxr-xr-x[-rw-r--r--]MetaTools/buildUCD.py0
-rwxr-xr-x[-rw-r--r--]MetaTools/roundTrip.py0
-rw-r--r--NEWS.rst48
-rw-r--r--PKG-INFO64
-rwxr-xr-x[-rw-r--r--]Snippets/cmap-format.py0
-rwxr-xr-x[-rw-r--r--]Snippets/interpolate.py0
-rwxr-xr-x[-rw-r--r--]Snippets/layout-features.py0
-rwxr-xr-x[-rw-r--r--]Snippets/otf2ttf.py0
-rwxr-xr-x[-rw-r--r--]Snippets/rename-fonts.py0
-rwxr-xr-x[-rw-r--r--]Snippets/subset-fpgm.py0
-rwxr-xr-x[-rw-r--r--]Snippets/svg2glif.py0
-rwxr-xr-x[-rw-r--r--]Snippets/woff2_compress.py0
-rwxr-xr-x[-rw-r--r--]Snippets/woff2_decompress.py0
-rw-r--r--Tests/feaLib/builder_test.py2
-rw-r--r--Tests/feaLib/data/bug1459.fea7
-rw-r--r--Tests/feaLib/data/bug1459.ttx55
-rw-r--r--Tests/feaLib/parser_test.py97
-rw-r--r--Tests/fontBuilder/data/test_uvs.ttf.ttx20
-rw-r--r--Tests/fontBuilder/fontBuilder_test.py43
-rw-r--r--Tests/misc/loggingTools_test.py4
-rw-r--r--Tests/subset/subset_test.py101
-rw-r--r--Tests/svgLib/path/parser_test.py69
-rw-r--r--Tests/ttLib/tables/_c_m_a_p_test.py45
-rw-r--r--Tests/ttLib/tables/_h_m_t_x_test.py39
-rw-r--r--Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx12
-rw-r--r--Tests/ttLib/tables/data/_c_m_a_p_format_14_bw_compat.ttx12
-rw-r--r--Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap16
-rw-r--r--Tests/ttLib/woff2_test.py6
-rw-r--r--Tests/ttx/ttx_test.py13
-rwxr-xr-x[-rw-r--r--]Tests/ufoLib/testSupport.py0
-rw-r--r--Tests/varLib/data/Build.designspace2
-rw-r--r--Tests/varLib/data/SparseMasters.designspace23
-rwxr-xr-x[-rw-r--r--]Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx0
-rw-r--r--Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Bold.ttx419
-rw-r--r--Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Medium.ttx125
-rw-r--r--Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Regular.ttx419
-rwxr-xr-x[-rw-r--r--]Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx0
-rw-r--r--Tests/varLib/data/test_results/BuildMain.ttx15
-rwxr-xr-x[-rw-r--r--]Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx0
-rwxr-xr-x[-rw-r--r--]Tests/varLib/data/test_results/Mutator_IUP-instance.ttx0
-rw-r--r--Tests/varLib/data/test_results/SparseMasters.ttx660
-rw-r--r--Tests/varLib/varLib_test.py49
-rw-r--r--Tests/voltLib/parser_test.py227
-rwxr-xr-x[-rw-r--r--]fonttools0
-rw-r--r--requirements.txt2
-rwxr-xr-x[-rw-r--r--]run-tests.sh0
-rw-r--r--setup.cfg2
-rwxr-xr-x[-rw-r--r--]setup.py4
82 files changed, 3154 insertions, 378 deletions
diff --git a/.travis/after_success.sh b/.travis/after_success.sh
index 07bcab5e..07bcab5e 100644..100755
--- a/.travis/after_success.sh
+++ b/.travis/after_success.sh
diff --git a/.travis/before_install.sh b/.travis/before_install.sh
index 8cc4edba..8cc4edba 100644..100755
--- a/.travis/before_install.sh
+++ b/.travis/before_install.sh
diff --git a/.travis/install.sh b/.travis/install.sh
index f2a0717f..f2a0717f 100644..100755
--- a/.travis/install.sh
+++ b/.travis/install.sh
diff --git a/.travis/run.sh b/.travis/run.sh
index ffb0ef79..ffb0ef79 100644..100755
--- a/.travis/run.sh
+++ b/.travis/run.sh
diff --git a/Doc/source/designspaceLib/readme.rst b/Doc/source/designspaceLib/readme.rst
index 06bf46ff..68a70091 100644
--- a/Doc/source/designspaceLib/readme.rst
+++ b/Doc/source/designspaceLib/readme.rst
@@ -216,8 +216,6 @@ Attributes
- ``glyphs``: dict for special master definitions for glyphs. If glyphs
need special masters (to record the results of executed rules for
example). MutatorMath.
-- ``mutedGlyphNames``: list of glyphnames that should be suppressed in
- the generation of this instance.
- ``kerning``: bool. Indicates if this instance needs its kerning
calculated. MutatorMath.
- ``info``: bool. Indicated if this instance needs the interpolating
@@ -277,7 +275,8 @@ AxisDescriptor object
dicts. MutatorMath + Varlib.
- ``labelNames``: dict. When defining a non-registered axis, it will be
necessary to define user-facing readable names for the axis. Keyed by
- xml:lang code. Varlib.
+ xml:lang code. Values are required to be ``unicode`` strings, even if
+ they only contain ASCII characters.
- ``minimum``: number. The minimum value for this axis in user space.
MutatorMath + Varlib.
- ``maximum``: number. The maximum value for this axis in user space.
diff --git a/Doc/source/designspaceLib/scripting.rst b/Doc/source/designspaceLib/scripting.rst
index 2bd4a0a1..5a17816a 100644
--- a/Doc/source/designspaceLib/scripting.rst
+++ b/Doc/source/designspaceLib/scripting.rst
@@ -70,7 +70,7 @@ readable names for this axis if this is not an axis that is registered
by OpenType. Think "The label next to the slider". The attribute is a
dictionary. The key is the `xml language
tag <https://www.w3.org/International/articles/language-tags/>`__, the
-value is a utf-8 string with the name. Whether or not this attribute is
+value is a ``unicode`` string with the name. Whether or not this attribute is
used depends on the font building tool, the operating system and the
authoring software. This, at least, is the place to record it.
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index e922c48e..f3759f15 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -5,6 +5,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "3.35.0"
+version = __version__ = "3.37.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py
index d9da48c0..b8b962fc 100644
--- a/Lib/fontTools/designspaceLib/__init__.py
+++ b/Lib/fontTools/designspaceLib/__init__.py
@@ -235,7 +235,6 @@ class InstanceDescriptor(SimpleDescriptor):
self.localisedStyleMapStyleName = {}
self.localisedStyleMapFamilyName = {}
self.glyphs = {}
- self.mutedGlyphNames = []
self.kerning = True
self.info = True
@@ -246,25 +245,25 @@ class InstanceDescriptor(SimpleDescriptor):
filename = posixpath_property("_filename")
def setStyleName(self, styleName, languageCode="en"):
- self.localisedStyleName[languageCode] = styleName
+ self.localisedStyleName[languageCode] = tounicode(styleName)
def getStyleName(self, languageCode="en"):
return self.localisedStyleName.get(languageCode)
def setFamilyName(self, familyName, languageCode="en"):
- self.localisedFamilyName[languageCode] = familyName
+ self.localisedFamilyName[languageCode] = tounicode(familyName)
def getFamilyName(self, languageCode="en"):
return self.localisedFamilyName.get(languageCode)
def setStyleMapStyleName(self, styleMapStyleName, languageCode="en"):
- self.localisedStyleMapStyleName[languageCode] = styleMapStyleName
+ self.localisedStyleMapStyleName[languageCode] = tounicode(styleMapStyleName)
def getStyleMapStyleName(self, languageCode="en"):
return self.localisedStyleMapStyleName.get(languageCode)
def setStyleMapFamilyName(self, styleMapFamilyName, languageCode="en"):
- self.localisedStyleMapFamilyName[languageCode] = styleMapFamilyName
+ self.localisedStyleMapFamilyName[languageCode] = tounicode(styleMapFamilyName)
def getStyleMapFamilyName(self, languageCode="en"):
return self.localisedStyleMapFamilyName.get(languageCode)
@@ -753,8 +752,7 @@ class BaseDocReader(LogMixin):
# '{http://www.w3.org/XML/1998/namespace}lang'
for key, lang in labelNameElement.items():
if key == XML_LANG:
- labelName = labelNameElement.text
- axisObject.labelNames[lang] = labelName
+ axisObject.labelNames[lang] = tounicode(labelNameElement.text)
self.documentObject.axes.append(axisObject)
self.axisDefaults[axisObject.name] = axisObject.default
self.documentObject.defaultLoc = self.axisDefaults
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index 7f33ca41..b7d20664 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -293,7 +293,10 @@ class FeatureBlock(Block):
builder.end_feature()
def asFea(self, indent=""):
- res = indent + "feature %s {\n" % self.name.strip()
+ res = indent + "feature %s " % self.name.strip()
+ if self.use_extension:
+ res += "useExtension "
+ res += "{\n"
res += Block.asFea(self, indent=indent)
res += indent + "} %s;\n" % self.name.strip()
return res
@@ -329,7 +332,10 @@ class LookupBlock(Block):
builder.end_lookup_block()
def asFea(self, indent=""):
- res = "lookup {} {{\n".format(self.name)
+ res = "lookup {} ".format(self.name)
+ if self.use_extension:
+ res += "useExtension "
+ res += "{\n"
res += Block.asFea(self, indent=indent)
res += "{}}} {};\n".format(indent, self.name)
return res
@@ -957,12 +963,12 @@ class PairPosStatement(Statement):
res = "enum " if self.enumerated else ""
if self.valuerecord2:
res += "pos {} {} {} {};".format(
- self.glyphs1.asFea(), self.valuerecord1.makeString(),
- self.glyphs2.asFea(), self.valuerecord2.makeString())
+ self.glyphs1.asFea(), self.valuerecord1.asFea(),
+ self.glyphs2.asFea(), self.valuerecord2.asFea())
else:
res += "pos {} {} {};".format(
self.glyphs1.asFea(), self.glyphs2.asFea(),
- self.valuerecord1.makeString())
+ self.valuerecord1.asFea())
return res
@@ -1063,12 +1069,12 @@ class SinglePosStatement(Statement):
if len(self.prefix):
res += " ".join(map(asFea, self.prefix)) + " "
res += " ".join([asFea(x[0]) + "'" + (
- (" " + x[1].makeString()) if x[1] else "") for x in self.pos])
+ (" " + x[1].asFea()) if x[1] else "") for x in self.pos])
if len(self.suffix):
res += " " + " ".join(map(asFea, self.suffix))
else:
res += " ".join([asFea(x[0]) + " " +
- (x[1].makeString() if x[1] else "") for x in self.pos])
+ (x[1].asFea() if x[1] else "") for x in self.pos])
res += ";"
return res
@@ -1114,13 +1120,15 @@ class ValueRecord(Expression):
hash(self.xPlaDevice) ^ hash(self.yPlaDevice) ^
hash(self.xAdvDevice) ^ hash(self.yAdvDevice))
- def makeString(self, vertical=None):
+ def asFea(self, indent=""):
+ if not self:
+ return "<NULL>"
+
x, y = self.xPlacement, self.yPlacement
xAdvance, yAdvance = self.xAdvance, self.yAdvance
xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice
xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice
- if vertical is None:
- vertical = self.vertical
+ vertical = self.vertical
# Try format A, if possible.
if x is None and y is None:
@@ -1140,6 +1148,23 @@ class ValueRecord(Expression):
deviceToString(xPlaDevice), deviceToString(yPlaDevice),
deviceToString(xAdvDevice), deviceToString(yAdvDevice))
+ def __bool__(self):
+ return any(
+ getattr(self, v) is not None
+ for v in [
+ "xPlacement",
+ "yPlacement",
+ "xAdvance",
+ "yAdvance",
+ "xPlaDevice",
+ "yPlaDevice",
+ "xAdvDevice",
+ "yAdvDevice",
+ ]
+ )
+
+ __nonzero__ = __bool__
+
class ValueRecordDefinition(Statement):
def __init__(self, name, value, location=None):
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index 957b01d6..e521a0ed 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -1114,7 +1114,7 @@ _VALUEREC_ATTRS = {
def makeOpenTypeValueRecord(v, pairPosContext):
"""ast.ValueRecord --> (otBase.ValueRecord, int ValueFormat)"""
- if v is None:
+ if not v:
return None, 0
vr = {}
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index 30426987..61d37111 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -1153,7 +1153,7 @@ class Parser(object):
name = self.expect_name_()
if name == "NULL":
self.expect_symbol_(">")
- return None
+ return self.ast.ValueRecord()
vrd = self.valuerecords_.resolve(name)
if vrd is None:
raise FeatureLibError("Unknown valueRecordDef \"%s\"" % name,
diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py
index 70519822..8854c164 100644
--- a/Lib/fontTools/fontBuilder.py
+++ b/Lib/fontTools/fontBuilder.py
@@ -364,9 +364,18 @@ class FontBuilder(object):
"""Set the glyph order for the font."""
self.font.setGlyphOrder(glyphOrder)
- def setupCharacterMap(self, cmapping, allowFallback=False):
+ def setupCharacterMap(self, cmapping, uvs=None, allowFallback=False):
"""Build the `cmap` table for the font. The `cmapping` argument should
be a dict mapping unicode code points as integers to glyph names.
+
+ The `uvs` argument, when passed, must be a list of tuples, describing
+ Unicode Variation Sequences. These tuples have three elements:
+ (unicodeValue, variationSelector, glyphName)
+ `unicodeValue` and `variationSelector` are integer code points.
+ `glyphName` may be None, to indicate this is the default variation.
+ Text processors will then use the cmap to find the glyph name.
+ Each Unicode Variation Sequence should be an officially supported
+ sequence, but this is not policed.
"""
subTables = []
highestUnicode = max(cmapping)
@@ -390,6 +399,19 @@ class FontBuilder(object):
subTable_0_3 = buildCmapSubTable(cmapping_3_1, format, 0, 3)
subTables.append(subTable_0_3)
+ if uvs is not None:
+ uvsDict = {}
+ for unicodeValue, variationSelector, glyphName in uvs:
+ if cmapping.get(unicodeValue) == glyphName:
+ # this is a default variation
+ glyphName = None
+ if variationSelector not in uvsDict:
+ uvsDict[variationSelector] = []
+ uvsDict[variationSelector].append((unicodeValue, glyphName))
+ uvsSubTable = buildCmapSubTable({}, 14, 0, 5)
+ uvsSubTable.uvsDict = uvsDict
+ subTables.append(uvsSubTable)
+
self.font["cmap"] = newTable("cmap")
self.font["cmap"].tableVersion = 0
self.font["cmap"].tables = subTables
diff --git a/Lib/fontTools/misc/psCharStrings.py b/Lib/fontTools/misc/psCharStrings.py
index 68810744..34f21bb1 100644
--- a/Lib/fontTools/misc/psCharStrings.py
+++ b/Lib/fontTools/misc/psCharStrings.py
@@ -983,6 +983,16 @@ class T2CharString(object):
return
opcodes = self.opcodes
program = self.program
+
+ if isCFF2:
+ # If present, remove return and endchar operators.
+ if program and program[-1] in ("return", "endchar"):
+ program = program[:-1]
+ elif program and not isinstance(program[-1], basestring):
+ raise CharStringCompileError(
+ "T2CharString or Subr has items on the stack after last operator."
+ )
+
bytecode = []
encodeInt = self.getIntEncoder()
encodeFixed = self.getFixedEncoder()
@@ -1012,11 +1022,6 @@ class T2CharString(object):
raise
self.setBytecode(bytecode)
- if isCFF2:
- # If present, remove return and endchar operators.
- if self.bytecode and (byteord(self.bytecode[-1]) in (11, 14)):
- self.bytecode = self.bytecode[:-1]
-
def needsDecompilation(self):
return self.bytecode is not None
@@ -1088,13 +1093,12 @@ class T2CharString(object):
else:
args.append(token)
if args:
- if self.isCFF2:
- # CFF2Subr's can have numeric arguments on the stack after the last operator.
- args = [str(arg) for arg in args]
- line = ' '.join(args)
- xmlWriter.write(line)
- else:
- assert 0, "T2Charstring or Subr has items on the stack after last operator."
+ # NOTE: only CFF2 charstrings/subrs can have numeric arguments on
+ # the stack after the last operator. Compiling this would fail if
+ # this is part of CFF 1.0 table.
+ args = [str(arg) for arg in args]
+ line = ' '.join(args)
+ xmlWriter.write(line)
def fromXML(self, name, attrs, content):
from fontTools.misc.textTools import binary2num, readHex
diff --git a/Lib/fontTools/misc/py23.py b/Lib/fontTools/misc/py23.py
index 0a13b6fc..37020555 100644
--- a/Lib/fontTools/misc/py23.py
+++ b/Lib/fontTools/misc/py23.py
@@ -308,6 +308,35 @@ except AttributeError:
return result
+try:
+ _isfinite = _math.isfinite # Python >= 3.2
+except AttributeError:
+ _isfinite = None
+ _isnan = _math.isnan
+ _isinf = _math.isinf
+
+
+def isfinite(f):
+ """
+ >>> isfinite(0.0)
+ True
+ >>> isfinite(-0.1)
+ True
+ >>> isfinite(1e10)
+ True
+ >>> isfinite(float("nan"))
+ False
+ >>> isfinite(float("+inf"))
+ False
+ >>> isfinite(float("-inf"))
+ False
+ """
+ if _isfinite is not None:
+ return _isfinite(f)
+ else:
+ return not (_isnan(f) or _isinf(f))
+
+
import decimal as _decimal
if PY3:
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index 2ed17cc9..9403fc15 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -123,6 +123,8 @@ Output options:
Glyph set expansion:
These options control how additional glyphs are added to the subset.
+ --retain-gids
+ Retain glyph indices; just empty glyphs not needed in-place.
--notdef-glyph
Add the '.notdef' glyph to the subset (ie, keep it). [default]
--no-notdef-glyph
@@ -1714,16 +1716,23 @@ def subset_glyphs(self, s):
@_add_method(ttLib.getTableClass('vmtx'))
def subset_glyphs(self, s):
self.metrics = _dict_subset(self.metrics, s.glyphs)
+ for g in s.glyphs_emptied:
+ self.metrics[g] = (0,0)
return bool(self.metrics)
@_add_method(ttLib.getTableClass('hmtx'))
def subset_glyphs(self, s):
self.metrics = _dict_subset(self.metrics, s.glyphs)
+ for g in s.glyphs_emptied:
+ self.metrics[g] = (0,0)
return True # Required table
@_add_method(ttLib.getTableClass('hdmx'))
def subset_glyphs(self, s):
self.hdmx = {sz:_dict_subset(l, s.glyphs) for sz,l in self.hdmx.items()}
+ for sz in self.hdmx:
+ for g in s.glyphs_emptied:
+ self.hdmx[sz][g] = 0
return bool(self.hdmx)
@_add_method(ttLib.getTableClass('ankr'))
@@ -1784,6 +1793,8 @@ def subset_glyphs(self, s):
def subset_glyphs(self, s):
table = self.table
+ # TODO Update for retain_gids
+
used = set()
if table.AdvWidthMap:
@@ -2010,7 +2021,7 @@ def subset_glyphs(self, s):
return True
@_add_method(ttLib.getTableModule('glyf').Glyph)
-def remapComponentsFast(self, indices):
+def remapComponentsFast(self, glyphidmap):
if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
return # Not composite
data = array.array("B", self.data)
@@ -2020,7 +2031,7 @@ def remapComponentsFast(self, indices):
flags =(data[i] << 8) | data[i+1]
glyphID =(data[i+2] << 8) | data[i+3]
# Remap
- glyphID = indices.index(glyphID)
+ glyphID = glyphidmap[glyphID]
data[i+2] = glyphID >> 8
data[i+3] = glyphID & 0xFF
i += 4
@@ -2063,13 +2074,17 @@ def prune_pre_subset(self, font, options):
@_add_method(ttLib.getTableClass('glyf'))
def subset_glyphs(self, s):
self.glyphs = _dict_subset(self.glyphs, s.glyphs)
- indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs]
- for v in self.glyphs.values():
- if hasattr(v, "data"):
- v.remapComponentsFast(indices)
- else:
- pass # No need
- self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs]
+ if not s.options.retain_gids:
+ indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs]
+ glyphmap = {o:n for n,o in enumerate(indices)}
+ for v in self.glyphs.values():
+ if hasattr(v, "data"):
+ v.remapComponentsFast(glyphmap)
+ Glyph = ttLib.getTableModule('glyf').Glyph
+ for g in s.glyphs_emptied:
+ self.glyphs[g] = Glyph()
+ self.glyphs[g].data = ''
+ self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs or g in s.glyphs_emptied]
# Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset.
return True
@@ -2275,6 +2290,7 @@ class Options(object):
self.name_legacy = False
self.name_languages = [0x0409] # English
self.obfuscate_names = False # to make webfont unusable as a system font
+ self.retain_gids = False
self.notdef_glyph = True # gid0 for TrueType / .notdef for CFF
self.notdef_outline = False # No need for notdef to have an outline really
self.recommended_glyphs = False # gid1, gid2, gid3 for TrueType
@@ -2533,12 +2549,17 @@ class Subsetter(object):
log.glyphs(self.glyphs, font=font)
self.glyphs_cffed = frozenset(self.glyphs)
- self.glyphs_all = frozenset(self.glyphs)
+ self.glyphs_retained = frozenset(self.glyphs)
+
+ self.glyphs_emptied = frozenset()
+ if self.options.retain_gids:
+ self.glyphs_emptied = realGlyphs - self.glyphs_retained
+ # TODO Drop empty glyphs at the end of GlyphOrder vector.
order = font.getReverseGlyphMap()
- self.reverseOrigGlyphMap = {g:order[g] for g in self.glyphs_all}
+ self.reverseOrigGlyphMap = {g:order[g] for g in self.glyphs_retained}
- log.info("Retaining %d glyphs", len(self.glyphs_all))
+ log.info("Retaining %d glyphs", len(self.glyphs_retained))
del self.glyphs
@@ -2551,7 +2572,7 @@ class Subsetter(object):
elif hasattr(clazz, 'subset_glyphs'):
with timer("subset '%s'" % tag):
table = font[tag]
- self.glyphs = self.glyphs_all
+ self.glyphs = self.glyphs_retained
retain = table.subset_glyphs(self)
del self.glyphs
if not retain:
@@ -2565,11 +2586,12 @@ class Subsetter(object):
log.warning("%s NOT subset; don't know how to subset; dropped", tag)
del font[tag]
- with timer("subset GlyphOrder"):
- glyphOrder = font.getGlyphOrder()
- glyphOrder = [g for g in glyphOrder if g in self.glyphs_all]
- font.setGlyphOrder(glyphOrder)
- font._buildReverseGlyphOrderDict()
+ if not self.options.retain_gids:
+ with timer("subset GlyphOrder"):
+ glyphOrder = font.getGlyphOrder()
+ glyphOrder = [g for g in glyphOrder if g in self.glyphs_retained]
+ font.setGlyphOrder(glyphOrder)
+ font._buildReverseGlyphOrderDict()
def _prune_post_subset(self, font):
for tag in font.keys():
diff --git a/Lib/fontTools/subset/cff.py b/Lib/fontTools/subset/cff.py
index 9a2b77e4..96dc3210 100644
--- a/Lib/fontTools/subset/cff.py
+++ b/Lib/fontTools/subset/cff.py
@@ -66,6 +66,26 @@ def closure_glyphs(self, s):
s.glyphs.update(components)
decompose = components
+def _empty_charstring(font, glyphName, isCFF2, ignoreWidth=False):
+ c, fdSelectIndex = font.CharStrings.getItemAndSelector(glyphName)
+ if isCFF2 or ignoreWidth:
+ # CFF2 charstrings have no widths nor 'endchar' operators
+ c.decompile()
+ c.program = [] if isCFF2 else ['endchar']
+ else:
+ if hasattr(font, 'FDArray') and font.FDArray is not None:
+ private = font.FDArray[fdSelectIndex].Private
+ else:
+ private = font.Private
+ dfltWdX = private.defaultWidthX
+ nmnlWdX = private.nominalWidthX
+ pen = NullPen()
+ c.draw(pen) # this will set the charstring's width
+ if c.width != dfltWdX:
+ c.program = [c.width - nmnlWdX, 'endchar']
+ else:
+ c.program = ['endchar']
+
@_add_method(ttLib.getTableClass('CFF '))
def prune_pre_subset(self, font, options):
cff = self.cff
@@ -73,21 +93,10 @@ def prune_pre_subset(self, font, options):
cff.fontNames = cff.fontNames[:1]
if options.notdef_glyph and not options.notdef_outline:
+ isCFF2 = cff.major > 1
for fontname in cff.keys():
font = cff[fontname]
- c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef')
- if hasattr(font, 'FDArray') and font.FDArray is not None:
- private = font.FDArray[fdSelectIndex].Private
- else:
- private = font.Private
- dfltWdX = private.defaultWidthX
- nmnlWdX = private.nominalWidthX
- pen = NullPen()
- c.draw(pen) # this will set the charstring's width
- if c.width != dfltWdX:
- c.program = [c.width - nmnlWdX, 'endchar']
- else:
- c.program = ['endchar']
+ _empty_charstring(font, ".notdef", isCFF2=isCFF2)
# Clear useless Encoding
for fontname in cff.keys():
@@ -104,37 +113,42 @@ def subset_glyphs(self, s):
font = cff[fontname]
cs = font.CharStrings
- # Load all glyphs
- for g in font.charset:
- if g not in s.glyphs: continue
- c, _ = cs.getItemAndSelector(g)
-
- if cs.charStringsAreIndexed:
- indices = [i for i,g in enumerate(font.charset) if g in s.glyphs]
- csi = cs.charStringsIndex
- csi.items = [csi.items[i] for i in indices]
- del csi.file, csi.offsets
- if hasattr(font, "FDSelect"):
- sel = font.FDSelect
- # XXX We want to set sel.format to None, such that the
- # most compact format is selected. However, OTS was
- # broken and couldn't parse a FDSelect format 0 that
- # happened before CharStrings. As such, always force
- # format 3 until we fix cffLib to always generate
- # FDSelect after CharStrings.
- # https://github.com/khaledhosny/ots/pull/31
- #sel.format = None
- sel.format = 3
- sel.gidArray = [sel.gidArray[i] for i in indices]
- cs.charStrings = {g:indices.index(v)
- for g,v in cs.charStrings.items()
- if g in s.glyphs}
+ if s.options.retain_gids:
+ isCFF2 = cff.major > 1
+ for g in s.glyphs_emptied:
+ _empty_charstring(font, g, isCFF2=isCFF2, ignoreWidth=True)
else:
- cs.charStrings = {g:v
- for g,v in cs.charStrings.items()
- if g in s.glyphs}
- font.charset = [g for g in font.charset if g in s.glyphs]
- font.numGlyphs = len(font.charset)
+ # Load all glyphs
+ for g in font.charset:
+ if g not in s.glyphs: continue
+ c, _ = cs.getItemAndSelector(g)
+
+ if cs.charStringsAreIndexed:
+ indices = [i for i,g in enumerate(font.charset) if g in s.glyphs]
+ csi = cs.charStringsIndex
+ csi.items = [csi.items[i] for i in indices]
+ del csi.file, csi.offsets
+ if hasattr(font, "FDSelect"):
+ sel = font.FDSelect
+ # XXX We want to set sel.format to None, such that the
+ # most compact format is selected. However, OTS was
+ # broken and couldn't parse a FDSelect format 0 that
+ # happened before CharStrings. As such, always force
+ # format 3 until we fix cffLib to always generate
+ # FDSelect after CharStrings.
+ # https://github.com/khaledhosny/ots/pull/31
+ #sel.format = None
+ sel.format = 3
+ sel.gidArray = [sel.gidArray[i] for i in indices]
+ cs.charStrings = {g:indices.index(v)
+ for g,v in cs.charStrings.items()
+ if g in s.glyphs}
+ else:
+ cs.charStrings = {g:v
+ for g,v in cs.charStrings.items()
+ if g in s.glyphs}
+ font.charset = [g for g in font.charset if g in s.glyphs]
+ font.numGlyphs = len(font.charset)
return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
diff --git a/Lib/fontTools/svgLib/path/arc.py b/Lib/fontTools/svgLib/path/arc.py
new file mode 100644
index 00000000..38d1ea9c
--- /dev/null
+++ b/Lib/fontTools/svgLib/path/arc.py
@@ -0,0 +1,157 @@
+"""Convert SVG Path's elliptical arcs to Bezier curves.
+
+The code is mostly adapted from Blink's SVGPathNormalizer::DecomposeArcToCubic
+https://github.com/chromium/chromium/blob/93831f2/third_party/
+blink/renderer/core/svg/svg_path_parser.cc#L169-L278
+"""
+from __future__ import print_function, division, absolute_import, unicode_literals
+from fontTools.misc.py23 import *
+from fontTools.misc.py23 import isfinite
+from fontTools.misc.transform import Identity, Scale
+from math import atan2, ceil, cos, fabs, pi, radians, sin, sqrt, tan
+
+
+TWO_PI = 2 * pi
+PI_OVER_TWO = 0.5 * pi
+
+
+def _map_point(matrix, pt):
+ # apply Transform matrix to a point represented as a complex number
+ r = matrix.transformPoint((pt.real, pt.imag))
+ return r[0] + r[1] * 1j
+
+
+class EllipticalArc(object):
+
+ def __init__(self, current_point, rx, ry, rotation, large, sweep, target_point):
+ self.current_point = current_point
+ self.rx = rx
+ self.ry = ry
+ self.rotation = rotation
+ self.large = large
+ self.sweep = sweep
+ self.target_point = target_point
+
+ # SVG arc's rotation angle is expressed in degrees, whereas Transform.rotate
+ # uses radians
+ self.angle = radians(rotation)
+
+ # these derived attributes are computed by the _parametrize method
+ self.center_point = self.theta1 = self.theta2 = self.theta_arc = None
+
+ def _parametrize(self):
+ # convert from endopoint to center parametrization:
+ # https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
+
+ # If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a
+ # "lineto") joining the endpoints.
+ # http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
+ rx = fabs(self.rx)
+ ry = fabs(self.ry)
+ if not (rx and ry):
+ return False
+
+ # If the current point and target point for the arc are identical, it should
+ # be treated as a zero length path. This ensures continuity in animations.
+ if self.target_point == self.current_point:
+ return False
+
+ mid_point_distance = (self.current_point - self.target_point) * 0.5
+
+ point_transform = Identity.rotate(-self.angle)
+
+ transformed_mid_point = _map_point(point_transform, mid_point_distance)
+ square_rx = rx * rx
+ square_ry = ry * ry
+ square_x = transformed_mid_point.real * transformed_mid_point.real
+ square_y = transformed_mid_point.imag * transformed_mid_point.imag
+
+ # Check if the radii are big enough to draw the arc, scale radii if not.
+ # http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
+ radii_scale = square_x / square_rx + square_y / square_ry
+ if radii_scale > 1:
+ rx *= sqrt(radii_scale)
+ ry *= sqrt(radii_scale)
+ self.rx, self.ry = rx, ry
+
+ point_transform = Scale(1 / rx, 1 / ry).rotate(-self.angle)
+
+ point1 = _map_point(point_transform, self.current_point)
+ point2 = _map_point(point_transform, self.target_point)
+ delta = point2 - point1
+
+ d = delta.real * delta.real + delta.imag * delta.imag
+ scale_factor_squared = max(1 / d - 0.25, 0.0)
+
+ scale_factor = sqrt(scale_factor_squared)
+ if self.sweep == self.large:
+ scale_factor = -scale_factor
+
+ delta *= scale_factor
+ center_point = (point1 + point2) * 0.5
+ center_point += complex(-delta.imag, delta.real)
+ point1 -= center_point
+ point2 -= center_point
+
+ theta1 = atan2(point1.imag, point1.real)
+ theta2 = atan2(point2.imag, point2.real)
+
+ theta_arc = theta2 - theta1
+ if theta_arc < 0 and self.sweep:
+ theta_arc += TWO_PI
+ elif theta_arc > 0 and not self.sweep:
+ theta_arc -= TWO_PI
+
+ self.theta1 = theta1
+ self.theta2 = theta1 + theta_arc
+ self.theta_arc = theta_arc
+ self.center_point = center_point
+
+ return True
+
+ def _decompose_to_cubic_curves(self):
+ if self.center_point is None and not self._parametrize():
+ return
+
+ point_transform = Identity.rotate(self.angle).scale(self.rx, self.ry)
+
+ # Some results of atan2 on some platform implementations are not exact
+ # enough. So that we get more cubic curves than expected here. Adding 0.001f
+ # reduces the count of sgements to the correct count.
+ num_segments = int(ceil(fabs(self.theta_arc / (PI_OVER_TWO + 0.001))))
+ for i in range(num_segments):
+ start_theta = self.theta1 + i * self.theta_arc / num_segments
+ end_theta = self.theta1 + (i + 1) * self.theta_arc / num_segments
+
+ t = (4 / 3) * tan(0.25 * (end_theta - start_theta))
+ if not isfinite(t):
+ return
+
+ sin_start_theta = sin(start_theta)
+ cos_start_theta = cos(start_theta)
+ sin_end_theta = sin(end_theta)
+ cos_end_theta = cos(end_theta)
+
+ point1 = complex(
+ cos_start_theta - t * sin_start_theta,
+ sin_start_theta + t * cos_start_theta,
+ )
+ point1 += self.center_point
+ target_point = complex(cos_end_theta, sin_end_theta)
+ target_point += self.center_point
+ point2 = target_point
+ point2 += complex(t * sin_end_theta, -t * cos_end_theta)
+
+ point1 = _map_point(point_transform, point1)
+ point2 = _map_point(point_transform, point2)
+ target_point = _map_point(point_transform, target_point)
+
+ yield point1, point2, target_point
+
+ def draw(self, pen):
+ for point1, point2, target_point in self._decompose_to_cubic_curves():
+ pen.curveTo(
+ (point1.real, point1.imag),
+ (point2.real, point2.imag),
+ (target_point.real, target_point.imag),
+ )
diff --git a/Lib/fontTools/svgLib/path/parser.py b/Lib/fontTools/svgLib/path/parser.py
index 4daefcae..ae0aba39 100644
--- a/Lib/fontTools/svgLib/path/parser.py
+++ b/Lib/fontTools/svgLib/path/parser.py
@@ -10,8 +10,10 @@
from __future__ import (
print_function, division, absolute_import, unicode_literals)
from fontTools.misc.py23 import *
+from .arc import EllipticalArc
import re
+
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')
@@ -27,7 +29,7 @@ def _tokenize_path(pathdef):
yield token
-def parse_path(pathdef, pen, current_pos=(0, 0)):
+def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc):
""" Parse SVG path definition (i.e. "d" attribute of <path> elements)
and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath
methods.
@@ -35,8 +37,13 @@ def parse_path(pathdef, pen, current_pos=(0, 0)):
If 'current_pos' (2-float tuple) is provided, the initial moveTo will
be relative to that instead being absolute.
- Arc segments (commands "A" or "a") are not currently supported, and raise
- NotImplementedError.
+ If the pen has an "arcTo" method, it is called with the original values
+ of the elliptical arc curve commands:
+
+ pen.arcTo(rx, ry, rotation, arc_large, arc_sweep, (x, y))
+
+ Otherwise, the arcs are approximated by series of cubic Bezier segments
+ ("curveTo"), one every 90 degrees.
"""
# In the SVG specs, initial movetos are absolute, even if
# specified as 'm'. This is the default behavior here as well.
@@ -52,6 +59,8 @@ def parse_path(pathdef, pen, current_pos=(0, 0)):
command = None
last_control = None
+ have_arcTo = hasattr(pen, "arcTo")
+
while elements:
if elements[-1] in COMMANDS:
@@ -209,7 +218,34 @@ def parse_path(pathdef, pen, current_pos=(0, 0)):
last_control = control
elif command == 'A':
- raise NotImplementedError('arcs are not supported')
+ rx = float(elements.pop())
+ ry = float(elements.pop())
+ rotation = float(elements.pop())
+ arc_large = bool(int(elements.pop()))
+ arc_sweep = bool(int(elements.pop()))
+ end = float(elements.pop()) + float(elements.pop()) * 1j
+
+ if not absolute:
+ end += current_pos
+
+ # if the pen supports arcs, pass the values unchanged, otherwise
+ # approximate the arc with a series of cubic bezier curves
+ if have_arcTo:
+ pen.arcTo(
+ rx,
+ ry,
+ rotation,
+ arc_large,
+ arc_sweep,
+ (end.real, end.imag),
+ )
+ else:
+ arc = arc_class(
+ current_pos, rx, ry, rotation, arc_large, arc_sweep, end
+ )
+ arc.draw(pen)
+
+ current_pos = end
# no final Z command, it's an open path
if start_pos is not None:
diff --git a/Lib/fontTools/ttLib/tables/_c_m_a_p.py b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
index 5bc9354f..1767ad4c 100644
--- a/Lib/fontTools/ttLib/tables/_c_m_a_p.py
+++ b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
@@ -104,7 +104,7 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
tables.append(table)
def compile(self, ttFont):
- self.tables.sort() # sort according to the spec; see CmapSubtable.__lt__()
+ self.tables.sort() # sort according to the spec; see CmapSubtable.__lt__()
numSubTables = len(self.tables)
totalOffset = 4 + 8 * numSubTables
data = struct.pack(">HH", self.tableVersion, numSubTables)
@@ -246,7 +246,7 @@ class cmap_format_0(CmapSubtable):
def decompile(self, data, ttFont):
# we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
- # If not, someone is calling the subtable decompile() directly, and must provide both args.
+ # If not, someone is calling the subtable decompile() directly, and must provide both args.
if data is not None and ttFont is not None:
self.decompileHeader(data, ttFont)
else:
@@ -310,15 +310,15 @@ class cmap_format_2(CmapSubtable):
# so that we are more likely to be able to combine glypharray GID subranges.
# This means that we have a problem when minGI is > 32K
# Since the final gi is reconstructed from the glyphArray GID by:
- # (short)finalGID = (gid + idDelta) % 0x10000),
+ # (short)finalGID = (gid + idDelta) % 0x10000),
# we can get from a glypharray GID of 1 to a final GID of 65K by subtracting 2, and casting the
# negative number to an unsigned short.
- if (minGI > 1):
- if minGI > 0x7FFF:
+ if (minGI > 1):
+ if minGI > 0x7FFF:
subHeader.idDelta = -(0x10000 - minGI) -1
else:
- subHeader.idDelta = minGI -1
+ subHeader.idDelta = minGI -1
idDelta = subHeader.idDelta
for i in range(subHeader.entryCount):
gid = subHeader.glyphIndexArray[i]
@@ -327,7 +327,7 @@ class cmap_format_2(CmapSubtable):
def decompile(self, data, ttFont):
# we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
- # If not, someone is calling the subtable decompile() directly, and must provide both args.
+ # If not, someone is calling the subtable decompile() directly, and must provide both args.
if data is not None and ttFont is not None:
self.decompileHeader(data, ttFont)
else:
@@ -360,7 +360,7 @@ class cmap_format_2(CmapSubtable):
subHeaderList.append(subHeader)
# How this gets processed.
# Charcodes may be one or two bytes.
- # The first byte of a charcode is mapped through the subHeaderKeys, to select
+ # The first byte of a charcode is mapped through the subHeaderKeys, to select
# a subHeader. For any subheader but 0, the next byte is then mapped through the
# selected subheader. If subheader Index 0 is selected, then the byte itself is
# mapped through the subheader, and there is no second byte.
@@ -466,17 +466,17 @@ class cmap_format_2(CmapSubtable):
gids.append(gid)
- # Process the (char code to gid) item list in char code order.
+ # Process the (char code to gid) item list in char code order.
# By definition, all one byte char codes map to subheader 0.
# For all the two byte char codes, we assume that the first byte maps maps to the empty subhead (with an entry count of 0,
# which defines all char codes in its range to map to notdef) unless proven otherwise.
# Note that since the char code items are processed in char code order, all the char codes with the
# same first byte are in sequential order.
- subHeaderKeys = [ kEmptyTwoCharCodeRange for x in range(256)] # list of indices into subHeaderList.
+ subHeaderKeys = [kEmptyTwoCharCodeRange for x in range(256)] # list of indices into subHeaderList.
subHeaderList = []
- # We force this subheader entry 0 to exist in the subHeaderList in the case where some one comes up
+ # We force this subheader entry 0 to exist in the subHeaderList in the case where some one comes up
# with a cmap where all the one byte char codes map to notdef,
# with the result that the subhead 0 would not get created just by processing the item list.
charCode = charCodes[0]
@@ -549,7 +549,7 @@ class cmap_format_2(CmapSubtable):
for index in range(subheadRangeLen):
subHeader = subHeaderList[index]
subHeader.idRangeOffset = 0
- for j in range(index):
+ for j in range(index):
prevSubhead = subHeaderList[j]
if prevSubhead.glyphIndexArray == subHeader.glyphIndexArray: # use the glyphIndexArray subarray
subHeader.idRangeOffset = prevSubhead.idRangeOffset - (index-j)*8
@@ -684,7 +684,7 @@ class cmap_format_4(CmapSubtable):
def decompile(self, data, ttFont):
# we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
- # If not, someone is calling the subtable decompile() directly, and must provide both args.
+ # If not, someone is calling the subtable decompile() directly, and must provide both args.
if data is not None and ttFont is not None:
self.decompileHeader(data, ttFont)
else:
@@ -730,7 +730,7 @@ class cmap_format_4(CmapSubtable):
else:
for charCode in rangeCharCodes:
index = charCode + partial
- assert (index < lenGIArray), "In format 4 cmap, range (%d), the calculated index (%d) into the glyph index array is not less than the length of the array (%d) !" % (i, index, lenGIArray)
+ assert (index < lenGIArray), "In format 4 cmap, range (%d), the calculated index (%d) into the glyph index array is not less than the length of the array (%d) !" % (i, index, lenGIArray)
if glyphIndexArray[index] != 0: # if not missing glyph
glyphID = glyphIndexArray[index] + delta
else:
@@ -807,7 +807,7 @@ class cmap_format_4(CmapSubtable):
indices = []
for charCode in range(startCode[i], endCode[i] + 1):
indices.append(cmap[charCode])
- if (indices == list(range(indices[0], indices[0] + len(indices)))):
+ if (indices == list(range(indices[0], indices[0] + len(indices)))):
idDelta.append((indices[0] - startCode[i]) % 0x10000)
idRangeOffset.append(0)
else:
@@ -855,7 +855,7 @@ class cmap_format_6(CmapSubtable):
def decompile(self, data, ttFont):
# we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
- # If not, someone is calling the subtable decompile() directly, and must provide both args.
+ # If not, someone is calling the subtable decompile() directly, and must provide both args.
if data is not None and ttFont is not None:
self.decompileHeader(data, ttFont)
else:
@@ -932,7 +932,7 @@ class cmap_format_12_or_13(CmapSubtable):
def decompile(self, data, ttFont):
# we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
- # If not, someone is calling the subtable decompile() directly, and must provide both args.
+ # If not, someone is calling the subtable decompile() directly, and must provide both args.
if data is not None and ttFont is not None:
self.decompileHeader(data, ttFont)
else:
@@ -991,7 +991,7 @@ class cmap_format_12_or_13(CmapSubtable):
lastGlyphID = startGlyphID - self._format_step
lastCharCode = startCharCode - 1
nGroups = 0
- dataList = []
+ dataList = []
maxIndex = len(charCodes)
for index in range(maxIndex):
charCode = charCodes[index]
@@ -1073,12 +1073,12 @@ class cmap_format_13(cmap_format_12_or_13):
return (glyphID == lastGlyphID) and (charCode == 1 + lastCharCode)
-def cvtToUVS(threeByteString):
+def cvtToUVS(threeByteString):
data = b"\0" + threeByteString
val, = struct.unpack(">L", data)
return val
-def cvtFromUVS(val):
+def cvtFromUVS(val):
assert 0 <= val < 0x1000000
fourByteString = struct.pack(">L", val)
return fourByteString[1:]
@@ -1105,7 +1105,7 @@ class cmap_format_14(CmapSubtable):
uvsDict = {}
recOffset = 0
for n in range(self.numVarSelectorRecords):
- uvs, defOVSOffset, nonDefUVSOffset = struct.unpack(">3sLL", data[recOffset:recOffset +11])
+ uvs, defOVSOffset, nonDefUVSOffset = struct.unpack(">3sLL", data[recOffset:recOffset +11])
recOffset += 11
varUVS = cvtToUVS(uvs)
if defOVSOffset:
@@ -1135,7 +1135,7 @@ class cmap_format_14(CmapSubtable):
startOffset += 5
uv = cvtToUVS(uv)
glyphName = self.ttFont.getGlyphName(gid)
- localUVList.append( [uv, glyphName] )
+ localUVList.append((uv, glyphName))
try:
uvsDict[varUVS].extend(localUVList)
except KeyError:
@@ -1147,9 +1147,6 @@ class cmap_format_14(CmapSubtable):
writer.begintag(self.__class__.__name__, [
("platformID", self.platformID),
("platEncID", self.platEncID),
- ("format", self.format),
- ("length", self.length),
- ("numVarSelectorRecords", self.numVarSelectorRecords),
])
writer.newline()
uvsDict = self.uvsDict
@@ -1158,25 +1155,27 @@ class cmap_format_14(CmapSubtable):
uvList = uvsDict[uvs]
uvList.sort(key=lambda item: (item[1] is not None, item[0], item[1]))
for uv, gname in uvList:
- if gname is None:
- gname = "None"
- # I use the arg rather than th keyword syntax in order to preserve the attribute order.
- writer.simpletag("map", [ ("uvs",hex(uvs)), ("uv",hex(uv)), ("name", gname)] )
+ attrs = [("uv", hex(uv)), ("uvs", hex(uvs))]
+ if gname is not None:
+ attrs.append(("name", gname))
+ writer.simpletag("map", attrs)
writer.newline()
writer.endtag(self.__class__.__name__)
writer.newline()
def fromXML(self, name, attrs, content, ttFont):
- self.format = safeEval(attrs["format"])
- self.length = safeEval(attrs["length"])
- self.numVarSelectorRecords = safeEval(attrs["numVarSelectorRecords"])
- self.language = 0xFF # provide a value so that CmapSubtable.__lt__() won't fail
+ self.language = 0xFF # provide a value so that CmapSubtable.__lt__() won't fail
if not hasattr(self, "cmap"):
self.cmap = {} # so that clients that expect this to exist in a cmap table won't fail.
if not hasattr(self, "uvsDict"):
self.uvsDict = {}
uvsDict = self.uvsDict
+ # For backwards compatibility reasons we accept "None" as an indicator
+ # for "default mapping", unless the font actually has a glyph named
+ # "None".
+ _hasGlyphNamedNone = None
+
for element in content:
if not isinstance(element, tuple):
continue
@@ -1185,13 +1184,16 @@ class cmap_format_14(CmapSubtable):
continue
uvs = safeEval(attrs["uvs"])
uv = safeEval(attrs["uv"])
- gname = attrs["name"]
+ gname = attrs.get("name")
if gname == "None":
- gname = None
+ if _hasGlyphNamedNone is None:
+ _hasGlyphNamedNone = "None" in ttFont.getGlyphOrder()
+ if not _hasGlyphNamedNone:
+ gname = None
try:
- uvsDict[uvs].append( [uv, gname])
+ uvsDict[uvs].append((uv, gname))
except KeyError:
- uvsDict[uvs] = [ [uv, gname] ]
+ uvsDict[uvs] = [(uv, gname)]
def compile(self, ttFont):
if self.data:
@@ -1281,7 +1283,7 @@ class cmap_format_unknown(CmapSubtable):
def decompile(self, data, ttFont):
# we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None.
- # If not, someone is calling the subtable decompile() directly, and must provide both args.
+ # If not, someone is calling the subtable decompile() directly, and must provide both args.
if data is not None and ttFont is not None:
self.decompileHeader(data, ttFont)
else:
diff --git a/Lib/fontTools/ttLib/tables/_h_m_t_x.py b/Lib/fontTools/ttLib/tables/_h_m_t_x.py
index 6f8bb972..24cad4f7 100644
--- a/Lib/fontTools/ttLib/tables/_h_m_t_x.py
+++ b/Lib/fontTools/ttLib/tables/_h_m_t_x.py
@@ -23,7 +23,11 @@ class table__h_m_t_x(DefaultTable.DefaultTable):
def decompile(self, data, ttFont):
numGlyphs = ttFont['maxp'].numGlyphs
- numberOfMetrics = int(getattr(ttFont[self.headerTag], self.numberOfMetricsName))
+ headerTable = ttFont.get(self.headerTag)
+ if headerTable is not None:
+ numberOfMetrics = int(getattr(headerTable, self.numberOfMetricsName))
+ else:
+ numberOfMetrics = numGlyphs
if numberOfMetrics > numGlyphs:
log.warning("The %s.%s exceeds the maxp.numGlyphs" % (
self.headerTag, self.numberOfMetricsName))
@@ -69,19 +73,26 @@ class table__h_m_t_x(DefaultTable.DefaultTable):
glyphName, self.advanceName))
hasNegativeAdvances = True
metrics.append([advanceWidth, sideBearing])
- lastAdvance = metrics[-1][0]
- lastIndex = len(metrics)
- while metrics[lastIndex-2][0] == lastAdvance:
- lastIndex -= 1
- if lastIndex <= 1:
- # all advances are equal
- lastIndex = 1
- break
- additionalMetrics = metrics[lastIndex:]
- additionalMetrics = [otRound(sb) for _, sb in additionalMetrics]
- metrics = metrics[:lastIndex]
- numberOfMetrics = len(metrics)
- setattr(ttFont[self.headerTag], self.numberOfMetricsName, numberOfMetrics)
+
+ headerTable = ttFont.get(self.headerTag)
+ if headerTable is not None:
+ lastAdvance = metrics[-1][0]
+ lastIndex = len(metrics)
+ while metrics[lastIndex-2][0] == lastAdvance:
+ lastIndex -= 1
+ if lastIndex <= 1:
+ # all advances are equal
+ lastIndex = 1
+ break
+ additionalMetrics = metrics[lastIndex:]
+ additionalMetrics = [otRound(sb) for _, sb in additionalMetrics]
+ metrics = metrics[:lastIndex]
+ numberOfMetrics = len(metrics)
+ setattr(headerTable, self.numberOfMetricsName, numberOfMetrics)
+ else:
+ # no hhea/vhea, can't store numberOfMetrics; assume == numGlyphs
+ numberOfMetrics = ttFont["maxp"].numGlyphs
+ additionalMetrics = []
allMetrics = []
for advance, sb in metrics:
diff --git a/Lib/fontTools/ttLib/tables/_m_a_x_p.py b/Lib/fontTools/ttLib/tables/_m_a_x_p.py
index a94a9cf6..7da30b43 100644
--- a/Lib/fontTools/ttLib/tables/_m_a_x_p.py
+++ b/Lib/fontTools/ttLib/tables/_m_a_x_p.py
@@ -107,6 +107,7 @@ class table__m_a_x_p(DefaultTable.DefaultTable):
self.maxContours = maxContours
self.maxCompositePoints = maxCompositePoints
self.maxCompositeContours = maxCompositeContours
+ self.maxComponentElements = maxComponentElements
self.maxComponentDepth = maxComponentDepth
if allXMinIsLsb:
headTable.flags = headTable.flags | 0x2
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index d08bcc57..d08bcc57 100644..100755
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
diff --git a/Lib/fontTools/ttx.py b/Lib/fontTools/ttx.py
index a785325e..8f598f8a 100644
--- a/Lib/fontTools/ttx.py
+++ b/Lib/fontTools/ttx.py
@@ -77,6 +77,7 @@ usage: ttx [options] inputfile1 [... inputfileN]
file as-is.
--recalc-timestamp Set font 'modified' timestamp to current time.
By default, the modification time of the TTX file will be used.
+ --no-recalc-timestamp Keep the original font 'modified' timestamp.
--flavor <type> Specify flavor of output font file. May be 'woff'
or 'woff2'. Note that WOFF2 requires the Brotli Python extension,
available at https://github.com/google/brotli
@@ -123,7 +124,7 @@ class Options(object):
bitmapGlyphDataFormat = 'raw'
unicodedata = None
newlinestr = None
- recalcTimestamp = False
+ recalcTimestamp = None
flavor = None
useZopfli = False
@@ -204,6 +205,8 @@ class Options(object):
% (value, ", ".join(map(repr, validOptions))))
elif option == "--recalc-timestamp":
self.recalcTimestamp = True
+ elif option == "--no-recalc-timestamp":
+ self.recalcTimestamp = False
elif option == "--flavor":
self.flavor = value
elif option == "--with-zopfli":
@@ -282,7 +285,7 @@ def ttCompile(input, output, options):
allowVID=options.allowVID)
ttf.importXML(input)
- if not options.recalcTimestamp and 'head' in ttf:
+ if options.recalcTimestamp is None and 'head' in ttf:
# use TTX file modification time for head "modified" timestamp
mtime = os.path.getmtime(input)
ttf['head'].modified = timestampSinceEpoch(mtime)
@@ -328,8 +331,8 @@ def guessFileType(fileName):
def parseOptions(args):
rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sgim:z:baey:",
- ['unicodedata=', "recalc-timestamp", 'flavor=', 'version',
- 'with-zopfli', 'newline='])
+ ['unicodedata=', "recalc-timestamp", "no-recalc-timestamp",
+ 'flavor=', 'version', 'with-zopfli', 'newline='])
options = Options(rawOptions, len(files))
jobs = []
diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py
index d9a57c5d..26b7ddd3 100644..100755
--- a/Lib/fontTools/ufoLib/__init__.py
+++ b/Lib/fontTools/ufoLib/__init__.py
@@ -173,9 +173,9 @@ class _UFOBaseIO(object):
"the data is not properly formatted: %s"
% (fileName, self.fs, e)
)
- if self.fs.exists(fileName) and data == self.fs.getbytes(fileName):
+ if self.fs.exists(fileName) and data == self.fs.readbytes(fileName):
return
- self.fs.setbytes(fileName, data)
+ self.fs.writebytes(fileName, data)
else:
with self.fs.openbin(fileName, mode="w") as fp:
try:
@@ -356,7 +356,7 @@ class UFOReader(_UFOBaseIO):
Returns None if the file does not exist.
"""
try:
- return self.fs.getbytes(fsdecode(path))
+ return self.fs.readbytes(fsdecode(path))
except fs.errors.ResourceNotFound:
return None
@@ -758,7 +758,7 @@ class UFOReader(_UFOBaseIO):
except AttributeError:
# in case readData is called before getDataDirectoryListing
dataFS = self.fs.opendir(DATA_DIRNAME)
- data = dataFS.getbytes(fileName)
+ data = dataFS.readbytes(fileName)
except fs.errors.ResourceNotFound:
raise UFOLibError("No data file named '%s' on %s" % (fileName, self.fs))
return data
@@ -781,7 +781,7 @@ class UFOReader(_UFOBaseIO):
except AttributeError:
# in case readImage is called before getImageDirectoryListing
imagesFS = self.fs.opendir(IMAGES_DIRNAME)
- data = imagesFS.getbytes(fileName)
+ data = imagesFS.readbytes(fileName)
except fs.errors.ResourceNotFound:
raise UFOLibError("No image file named '%s' on %s" % (fileName, self.fs))
if validate:
@@ -1006,15 +1006,15 @@ class UFOWriter(UFOReader):
"""
path = fsdecode(path)
if self._havePreviousFile:
- if self.fs.isfile(path) and data == self.fs.getbytes(path):
+ if self.fs.isfile(path) and data == self.fs.readbytes(path):
return
try:
- self.fs.setbytes(path, data)
+ self.fs.writebytes(path, data)
except fs.errors.FileExpected:
raise UFOLibError("A directory exists at '%s'" % path)
except fs.errors.ResourceNotFound:
self.fs.makedirs(fs.path.dirname(path), recreate=True)
- self.fs.setbytes(path, data)
+ self.fs.writebytes(path, data)
def getFileObjectForPath(self, path, mode="w", encoding=None):
"""
diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py
index f2648b8c..e36c3c7f 100644..100755
--- a/Lib/fontTools/ufoLib/glifLib.py
+++ b/Lib/fontTools/ufoLib/glifLib.py
@@ -290,7 +290,7 @@ class GlyphSet(_UFOBaseIO):
"""
fileName = self.contents[glyphName]
try:
- return self.fs.getbytes(fileName)
+ return self.fs.readbytes(fileName)
except fs.errors.ResourceNotFound:
raise GlifLibError(
"The file '%s' associated with glyph '%s' in contents.plist "
@@ -316,7 +316,7 @@ class GlyphSet(_UFOBaseIO):
'glyphObject' argument can be any kind of object (even None);
the readGlyph() method will attempt to set the following
attributes on it:
- "width" the advance with of the glyph
+ "width" the advance width of the glyph
"height" the advance height of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
@@ -420,10 +420,10 @@ class GlyphSet(_UFOBaseIO):
if (
self._havePreviousFile
and self.fs.exists(fileName)
- and data == self.fs.getbytes(fileName)
+ and data == self.fs.readbytes(fileName)
):
return
- self.fs.setbytes(fileName, data)
+ self.fs.writebytes(fileName, data)
def deleteGlyph(self, glyphName):
"""Permanently delete the glyph from the glyph set on disk. Will
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index 37d8d334..2dd5718e 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -76,21 +76,24 @@ def _add_fvar(font, axes, instances):
axis.axisTag = Tag(a.tag)
# TODO Skip axes that have no variation.
axis.minValue, axis.defaultValue, axis.maxValue = a.minimum, a.default, a.maximum
- axis.axisNameID = nameTable.addName(tounicode(a.labelNames['en']))
- # TODO:
- # Replace previous line with the following when the following issues are resolved:
- # https://github.com/fonttools/fonttools/issues/930
- # https://github.com/fonttools/fonttools/issues/931
- # axis.axisNameID = nameTable.addMultilingualName(a.labelname, font)
+ axis.axisNameID = nameTable.addMultilingualName(a.labelNames, font)
+ axis.flags = int(a.hidden)
fvar.axes.append(axis)
for instance in instances:
coordinates = instance.location
- name = tounicode(instance.styleName)
+
+ if "en" not in instance.localisedStyleName:
+ assert instance.styleName
+ localisedStyleName = dict(instance.localisedStyleName)
+ localisedStyleName["en"] = tounicode(instance.styleName)
+ else:
+ localisedStyleName = instance.localisedStyleName
+
psname = instance.postScriptFontName
inst = NamedInstance()
- inst.subfamilyNameID = nameTable.addName(name)
+ inst.subfamilyNameID = nameTable.addMultilingualName(localisedStyleName)
if psname is not None:
psname = tounicode(psname)
inst.postscriptNameID = nameTable.addName(psname)
@@ -510,13 +513,28 @@ def _add_MVAR(font, masterModel, master_ttfs, axisTags):
lastTableTag = None
fontTable = None
tables = None
+ # HACK: we need to special-case post.underlineThickness and .underlinePosition
+ # and unilaterally/arbitrarily define a sentinel value to distinguish the case
+ # when a post table is present in a given master simply because that's where
+ # the glyph names in TrueType must be stored, but the underline values are not
+ # meant to be used for building MVAR's deltas. The value of -0x8000 (-36768)
+ # the minimum FWord (int16) value, was chosen for its unlikelyhood to appear
+ # in real-world underline position/thickness values.
+ specialTags = {"unds": -0x8000, "undo": -0x8000}
for tag, (tableTag, itemName) in sorted(MVAR_ENTRIES.items(), key=lambda kv: kv[1]):
if tableTag != lastTableTag:
tables = fontTable = None
if tableTag in font:
fontTable = font[tableTag]
- tables = [master[tableTag] if tableTag in master else None
- for master in master_ttfs]
+ tables = []
+ for master in master_ttfs:
+ if tableTag not in master or (
+ tag in specialTags
+ and getattr(master[tableTag], itemName) == specialTags[tag]
+ ):
+ tables.append(None)
+ else:
+ tables.append(master[tableTag])
lastTableTag = tableTag
if tables is None:
continue
@@ -662,10 +680,10 @@ def load_designspace(designspace):
instances = ds.instances
standard_axis_map = OrderedDict([
- ('weight', ('wght', {'en':'Weight'})),
- ('width', ('wdth', {'en':'Width'})),
- ('slant', ('slnt', {'en':'Slant'})),
- ('optical', ('opsz', {'en':'Optical Size'})),
+ ('weight', ('wght', {'en': u'Weight'})),
+ ('width', ('wdth', {'en': u'Width'})),
+ ('slant', ('slnt', {'en': u'Slant'})),
+ ('optical', ('opsz', {'en': u'Optical Size'})),
])
# Setup axes
@@ -684,7 +702,7 @@ def load_designspace(designspace):
else:
assert axis.tag is not None
if not axis.labelNames:
- axis.labelNames["en"] = axis_name
+ axis.labelNames["en"] = tounicode(axis_name)
axes[axis_name] = axis
log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()]))
@@ -809,14 +827,36 @@ def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
return vf, model, master_ttfs
+def _open_font(path, master_finder):
+ # load TTFont masters from given 'path': this can be either a .TTX or an
+ # OpenType binary font; or if neither of these, try use the 'master_finder'
+ # callable to resolve the path to a valid .TTX or OpenType font binary.
+ from fontTools.ttx import guessFileType
+
+ master_path = os.path.normpath(path)
+ tp = guessFileType(master_path)
+ if tp is None:
+ # not an OpenType binary/ttx, fall back to the master finder.
+ master_path = master_finder(master_path)
+ tp = guessFileType(master_path)
+ if tp in ("TTX", "OTX"):
+ font = TTFont()
+ font.importXML(master_path)
+ elif tp in ("TTF", "OTF", "WOFF", "WOFF2"):
+ font = TTFont(master_path)
+ else:
+ raise VarLibError("Invalid master path: %r" % master_path)
+ return font
+
+
def load_masters(designspace, master_finder=lambda s: s):
"""Ensure that all SourceDescriptor.font attributes have an appropriate TTFont
object loaded, or else open TTFont objects from the SourceDescriptor.path
attributes.
- The paths can point to either an OpenType font or to a UFO. In the latter case,
- use the provided master_finder callable to map from UFO paths to the respective
- master font binaries (e.g. .ttf or .otf).
+ The paths can point to either an OpenType font, a TTX file, or a UFO. In the
+ latter case, use the provided master_finder callable to map from UFO paths to
+ the respective master font binaries (e.g. .ttf, .otf or .ttx).
Return list of master TTFont objects in the same order they are listed in the
DesignSpaceDocument.
@@ -843,15 +883,10 @@ def load_masters(designspace, master_finder=lambda s: s):
"Designspace source '%s' has neither 'font' nor 'path' "
"attributes" % (master.name or "<Unknown>")
)
- # 2. A SourceDescriptor's path might point to a UFO or an OpenType
- # binary. Find out the hard way.
- master_path = os.path.normpath(master.path)
- try:
- font = TTFont(master_path)
- except (IOError, TTLibError):
- # 3. Not an OpenType binary, fall back to the master finder.
- master_path = master_finder(master_path)
- font = TTFont(master_path)
+ # 2. A SourceDescriptor's path might point an OpenType binary, a
+ # TTX file, or another source file (e.g. UFO), in which case we
+ # resolve the path using 'master_finder' function
+ font = _open_font(master.path, master_finder)
master_fonts.append(font)
return master_fonts
diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py
index 1a3b7389..79a6f3d1 100644
--- a/Lib/fontTools/varLib/mutator.py
+++ b/Lib/fontTools/varLib/mutator.py
@@ -345,6 +345,16 @@ def instantiateVariableFont(varfont, location, inplace=False):
for i in fvar.instances:
exclude.add(i.subfamilyNameID)
exclude.add(i.postscriptNameID)
+ if 'ltag' in varfont:
+ # Drop the whole 'ltag' table if all its language tags are referenced by
+ # name records to be pruned.
+ # TODO: prune unused ltag tags and re-enumerate langIDs accordingly
+ excludedUnicodeLangIDs = [
+ n.langID for n in varfont['name'].names
+ if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF
+ ]
+ if set(excludedUnicodeLangIDs) == set(range(len((varfont['ltag'].tags)))):
+ del varfont['ltag']
varfont['name'].names[:] = [
n for n in varfont['name'].names
if n.nameID not in exclude
diff --git a/Lib/fontTools/voltLib/ast.py b/Lib/fontTools/voltLib/ast.py
index 4e786007..de626bae 100644
--- a/Lib/fontTools/voltLib/ast.py
+++ b/Lib/fontTools/voltLib/ast.py
@@ -83,7 +83,7 @@ class GlyphName(Expression):
self.glyph = glyph
def glyphSet(self):
- return frozenset((self.glyph,))
+ return (self.glyph,)
class Enum(Expression):
@@ -96,14 +96,23 @@ class Enum(Expression):
for e in self.glyphSet():
yield e
+ def __len__(self):
+ return len(self.enum)
+
+ def __eq__(self, other):
+ return self.glyphSet() == other.glyphSet()
+
+ def __hash__(self):
+ return hash(self.glyphSet())
+
def glyphSet(self, groups=None):
- glyphs = set()
+ glyphs = []
for element in self.enum:
if isinstance(element, (GroupName, Enum)):
- glyphs = glyphs.union(element.glyphSet(groups))
+ glyphs.extend(element.glyphSet(groups))
else:
- glyphs = glyphs.union(element.glyphSet())
- return frozenset(glyphs)
+ glyphs.extend(element.glyphSet())
+ return tuple(glyphs)
class GroupName(Expression):
@@ -133,8 +142,7 @@ class Range(Expression):
self.parser = parser
def glyphSet(self):
- glyphs = self.parser.glyph_range(self.start, self.end)
- return frozenset(glyphs)
+ return tuple(self.parser.glyph_range(self.start, self.end))
class ScriptDefinition(Statement):
@@ -162,12 +170,14 @@ class FeatureDefinition(Statement):
class LookupDefinition(Statement):
- def __init__(self, name, process_base, process_marks, direction,
- reversal, comments, context, sub, pos, location=None):
+ def __init__(self, name, process_base, process_marks, mark_glyph_set,
+ direction, reversal, comments, context, sub, pos,
+ location=None):
Statement.__init__(self, location)
self.name = name
self.process_base = process_base
self.process_marks = process_marks
+ self.mark_glyph_set = mark_glyph_set
self.direction = direction
self.reversal = reversal
self.comments = comments
diff --git a/Lib/fontTools/voltLib/parser.py b/Lib/fontTools/voltLib/parser.py
index db3ccf3b..a452b9a4 100644
--- a/Lib/fontTools/voltLib/parser.py
+++ b/Lib/fontTools/voltLib/parser.py
@@ -44,10 +44,7 @@ class Parser(object):
func = getattr(self, PARSE_FUNCS[self.cur_token_])
statements.append(func())
elif self.is_cur_keyword_("END"):
- if self.next_token_type_ is not None:
- raise VoltLibError("Expected the end of the file",
- self.cur_token_location_)
- return self.doc_
+ break
else:
raise VoltLibError(
"Expected " + ", ".join(sorted(PARSE_FUNCS.keys())),
@@ -76,7 +73,7 @@ class Parser(object):
if self.next_token_ == "TYPE":
self.expect_keyword_("TYPE")
gtype = self.expect_name_()
- assert gtype in ("BASE", "LIGATURE", "MARK")
+ assert gtype in ("BASE", "LIGATURE", "MARK", "COMPONENT")
components = None
if self.next_token_ == "COMPONENTS":
self.expect_keyword_("COMPONENTS")
@@ -206,11 +203,12 @@ class Parser(object):
self.advance_lexer_()
process_base = False
process_marks = True
+ mark_glyph_set = None
if self.next_token_ == "PROCESS_MARKS":
self.advance_lexer_()
if self.next_token_ == "MARK_GLYPH_SET":
self.advance_lexer_()
- process_marks = self.expect_string_()
+ mark_glyph_set = self.expect_string_()
elif self.next_token_type_ == Lexer.STRING:
process_marks = self.expect_string_()
elif self.next_token_ == "ALL":
@@ -252,8 +250,8 @@ class Parser(object):
"Got %s" % (as_pos_or_sub),
location)
def_lookup = ast.LookupDefinition(
- name, process_base, process_marks, direction, reversal,
- comments, context, sub, pos, location=location)
+ name, process_base, process_marks, mark_glyph_set, direction,
+ reversal, comments, context, sub, pos, location=location)
self.lookups_.define(name, def_lookup)
return def_lookup
@@ -422,16 +420,17 @@ class Parser(object):
gid = self.expect_number_()
self.expect_keyword_("GLYPH")
glyph_name = self.expect_name_()
- # check for duplicate anchor names on this glyph
- if (glyph_name in self.anchors_
- and self.anchors_[glyph_name].resolve(name) is not None):
- raise VoltLibError(
- 'Anchor "%s" already defined, '
- 'anchor names are case insensitive' % name,
- location
- )
self.expect_keyword_("COMPONENT")
component = self.expect_number_()
+ # check for duplicate anchor names on this glyph
+ if glyph_name in self.anchors_:
+ anchor = self.anchors_[glyph_name].resolve(name)
+ if anchor is not None and anchor.component == component:
+ raise VoltLibError(
+ 'Anchor "%s" already defined, '
+ 'anchor names are case insensitive' % name,
+ location
+ )
if self.next_token_ == "LOCKED":
locked = True
self.advance_lexer_()
@@ -516,36 +515,24 @@ class Parser(object):
elif self.next_token_ == "GLYPH":
self.expect_keyword_("GLYPH")
name = self.expect_string_()
- coverage.append(name)
+ coverage.append(ast.GlyphName(name, location=location))
elif self.next_token_ == "GROUP":
self.expect_keyword_("GROUP")
name = self.expect_string_()
- # resolved_group = self.groups_.resolve(name)
- group = (name,)
- coverage.append(group)
- # if resolved_group is not None:
- # coverage.extend(resolved_group.enum)
- # # TODO: check that group exists after all groups are defined
- # else:
- # group = (name,)
- # coverage.append(group)
- # # raise VoltLibError(
- # # 'Glyph group "%s" is not defined' % name,
- # # location)
+ coverage.append(ast.GroupName(name, self, location=location))
elif self.next_token_ == "RANGE":
self.expect_keyword_("RANGE")
start = self.expect_string_()
self.expect_keyword_("TO")
end = self.expect_string_()
- coverage.append((start, end))
- return tuple(coverage)
+ coverage.append(ast.Range(start, end, self, location=location))
+ return ast.Enum(coverage, location=location)
def resolve_group(self, group_name):
return self.groups_.resolve(group_name)
def glyph_range(self, start, end):
- rng = self.glyphs_.range(start, end)
- return frozenset(rng)
+ return self.glyphs_.range(start, end)
def parse_ppem_(self):
location = self.cur_token_location_
@@ -601,6 +588,8 @@ class Parser(object):
self.cur_token_type_, self.cur_token_, self.cur_token_location_ = (
self.next_token_type_, self.next_token_, self.next_token_location_)
try:
+ if self.is_cur_keyword_("END"):
+ raise StopIteration
(self.next_token_type_, self.next_token_,
self.next_token_location_) = self.lexer_.next()
except StopIteration:
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index 32f95682..28c4c034 100644
--- a/Lib/fonttools.egg-info/PKG-INFO
+++ b/Lib/fonttools.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 3.35.0
+Version: 3.37.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -430,6 +430,54 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
Changelog
~~~~~~~~~
+ 3.37.0 (released 2019-01-28)
+ ----------------------------
+
+ - [svgLib] Added support for converting elliptical arcs to cubic bezier curves
+ (#1464).
+ - [py23] Added backport for ``math.isfinite``.
+ - [varLib] Apply HIDDEN flag to fvar axis if designspace axis has attribute
+ ``hidden=1``.
+ - Fixed "DeprecationWarning: invalid escape sequence" in Python 3.7.
+ - [voltLib] Fixed parsing glyph groups. Distinguish different PROCESS_MARKS.
+ Accept COMPONENT glyph type.
+ - [feaLib] Distinguish missing value and explicit ``<NULL>`` for PairPos2
+ format A (#1459). Round-trip ``useExtension`` keyword. Implemented
+ ``ValueRecord.asFea`` method.
+ - [subset] Insert empty widths into hdmx when retaining gids (#1458).
+
+ 3.36.0 (released 2019-01-17)
+ ----------------------------
+
+ - [ttx] Added ``--no-recalc-timestamp`` option to keep the original font's
+ ``head.modified`` timestamp (#1455, #46).
+ - [ttx/psCharStrings] Fixed issues while dumping and round-tripping CFF2 table
+ with ttx (#1451, #1452, #1456).
+ - [voltLib] Fixed check for duplicate anchors (#1450). Don't try to read past
+ the ``END`` operator in .vtp file (#1453).
+ - [varLib] Use sentinel value -0x8000 (-32768) to ignore post.underlineThickness
+ and post.underlinePosition when generating MVAR deltas (#1449,
+ googlei18n/ufo2ft#308).
+ - [subset] Added ``--retain-gids`` option to subset font without modifying the
+ current glyph indices (#1443, #1447).
+ - [ufoLib] Replace deprecated calls to ``getbytes`` and ``setbytes`` with new
+ equivalent ``readbytes`` and ``writebytes`` calls. ``fs`` >= 2.2 no required.
+ - [varLib] Allow loading masters from TTX files as well (#1441).
+
+ 3.35.2 (released 2019-01-14)
+ ----------------------------
+
+ - [hmtx/vmtx]: Allow to compile/decompile ``hmtx`` and ``vmtx`` tables even
+ without the corresponding (required) metrics header tables, ``hhea`` and
+ ``vhea`` (#1439).
+ - [varLib] Added support for localized axes' ``labelname`` and named instances'
+ ``stylename`` (#1438).
+
+ 3.35.1 (released 2019-01-09)
+ ----------------------------
+
+ - [_m_a_x_p] Include ``maxComponentElements`` in ``maxp`` table's recalculation.
+
3.35.0 (released 2019-01-07)
----------------------------
@@ -1582,13 +1630,13 @@ Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
-Provides-Extra: all
-Provides-Extra: interpolatable
-Provides-Extra: woff
-Provides-Extra: lxml
Provides-Extra: unicode
-Provides-Extra: graphite
-Provides-Extra: ufo
-Provides-Extra: type1
+Provides-Extra: interpolatable
Provides-Extra: plot
+Provides-Extra: type1
Provides-Extra: symfont
+Provides-Extra: ufo
+Provides-Extra: all
+Provides-Extra: graphite
+Provides-Extra: lxml
+Provides-Extra: woff
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index 04db0956..c71e28f7 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -165,6 +165,7 @@ Lib/fontTools/subset/__main__.py
Lib/fontTools/subset/cff.py
Lib/fontTools/svgLib/__init__.py
Lib/fontTools/svgLib/path/__init__.py
+Lib/fontTools/svgLib/path/arc.py
Lib/fontTools/svgLib/path/parser.py
Lib/fontTools/t1Lib/__init__.py
Lib/fontTools/ttLib/__init__.py
@@ -397,6 +398,8 @@ Tests/feaLib/data/baseClass.fea
Tests/feaLib/data/baseClass.feax
Tests/feaLib/data/bug1307.fea
Tests/feaLib/data/bug1307.ttx
+Tests/feaLib/data/bug1459.fea
+Tests/feaLib/data/bug1459.ttx
Tests/feaLib/data/bug453.fea
Tests/feaLib/data/bug453.ttx
Tests/feaLib/data/bug457.fea
@@ -528,6 +531,7 @@ Tests/feaLib/data/include/subdir/include2.fea
Tests/fontBuilder/fontBuilder_test.py
Tests/fontBuilder/data/test.otf.ttx
Tests/fontBuilder/data/test.ttf.ttx
+Tests/fontBuilder/data/test_uvs.ttf.ttx
Tests/fontBuilder/data/test_var.otf.ttx
Tests/fontBuilder/data/test_var.ttf.ttx
Tests/misc/arrayTools_test.py
@@ -724,6 +728,8 @@ Tests/ttLib/tables/data/C_F_F_.bin
Tests/ttLib/tables/data/C_F_F_.ttx
Tests/ttLib/tables/data/C_F_F__2.bin
Tests/ttLib/tables/data/C_F_F__2.ttx
+Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx
+Tests/ttLib/tables/data/_c_m_a_p_format_14_bw_compat.ttx
Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.glyf.bin
Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.head.bin
Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.loca.bin
@@ -1409,6 +1415,7 @@ Tests/varLib/data/FeatureVars.designspace
Tests/varLib/data/InterpolateLayout.designspace
Tests/varLib/data/InterpolateLayout2.designspace
Tests/varLib/data/InterpolateLayout3.designspace
+Tests/varLib/data/SparseMasters.designspace
Tests/varLib/data/TestCFF2.designspace
Tests/varLib/data/TestCFF2VF.otf
Tests/varLib/data/master_cff2/TestCFF2_Black.otf
@@ -1417,6 +1424,9 @@ Tests/varLib/data/master_cff2/TestCFF2_Regular.otf
Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx
Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master0.ttx
Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master1.ttx
+Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Bold.ttx
+Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Medium.ttx
+Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Regular.ttx
Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master0.ttx
Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master1.ttx
Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master2.ttx
@@ -1731,5 +1741,6 @@ Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx
Tests/varLib/data/test_results/Mutator.ttx
Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx
Tests/varLib/data/test_results/Mutator_IUP-instance.ttx
+Tests/varLib/data/test_results/SparseMasters.ttx
Tests/voltLib/lexer_test.py
Tests/voltLib/parser_test.py \ No newline at end of file
diff --git a/Lib/fonttools.egg-info/requires.txt b/Lib/fonttools.egg-info/requires.txt
index 9f927643..c94f1394 100644
--- a/Lib/fonttools.egg-info/requires.txt
+++ b/Lib/fonttools.egg-info/requires.txt
@@ -1,6 +1,6 @@
[all]
-fs<3,>=2.1.1
+fs<3,>=2.2.0
lxml<5,>=4.0
zopfli>=0.1.4
lz4>=1.7.4.2
@@ -56,7 +56,7 @@ sympy
xattr
[ufo]
-fs<3,>=2.1.1
+fs<3,>=2.2.0
[ufo:python_version < "3.4"]
enum34>=1.1.6
diff --git a/METADATA b/METADATA
index ba000aa7..4da9b169 100644
--- a/METADATA
+++ b/METADATA
@@ -7,12 +7,12 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/releases/download/3.35.0/fonttools-3.35.0.zip"
+ value: "https://github.com/fonttools/fonttools/releases/download/3.37.0/fonttools-3.37.0.zip"
}
- version: "3.35.0"
+ version: "3.37.0"
last_upgrade_date {
year: 2019
- month: 1
- day: 8
+ month: 2
+ day: 1
}
}
diff --git a/MetaTools/buildTableList.py b/MetaTools/buildTableList.py
index eb9fb858..eb9fb858 100644..100755
--- a/MetaTools/buildTableList.py
+++ b/MetaTools/buildTableList.py
diff --git a/MetaTools/buildUCD.py b/MetaTools/buildUCD.py
index 12bd58f1..12bd58f1 100644..100755
--- a/MetaTools/buildUCD.py
+++ b/MetaTools/buildUCD.py
diff --git a/MetaTools/roundTrip.py b/MetaTools/roundTrip.py
index 648bc9d9..648bc9d9 100644..100755
--- a/MetaTools/roundTrip.py
+++ b/MetaTools/roundTrip.py
diff --git a/NEWS.rst b/NEWS.rst
index 59bd94f4..8ebf4be6 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,51 @@
+3.37.0 (released 2019-01-28)
+----------------------------
+
+- [svgLib] Added support for converting elliptical arcs to cubic bezier curves
+ (#1464).
+- [py23] Added backport for ``math.isfinite``.
+- [varLib] Apply HIDDEN flag to fvar axis if designspace axis has attribute
+ ``hidden=1``.
+- Fixed "DeprecationWarning: invalid escape sequence" in Python 3.7.
+- [voltLib] Fixed parsing glyph groups. Distinguish different PROCESS_MARKS.
+ Accept COMPONENT glyph type.
+- [feaLib] Distinguish missing value and explicit ``<NULL>`` for PairPos2
+ format A (#1459). Round-trip ``useExtension`` keyword. Implemented
+ ``ValueRecord.asFea`` method.
+- [subset] Insert empty widths into hdmx when retaining gids (#1458).
+
+3.36.0 (released 2019-01-17)
+----------------------------
+
+- [ttx] Added ``--no-recalc-timestamp`` option to keep the original font's
+ ``head.modified`` timestamp (#1455, #46).
+- [ttx/psCharStrings] Fixed issues while dumping and round-tripping CFF2 table
+ with ttx (#1451, #1452, #1456).
+- [voltLib] Fixed check for duplicate anchors (#1450). Don't try to read past
+ the ``END`` operator in .vtp file (#1453).
+- [varLib] Use sentinel value -0x8000 (-32768) to ignore post.underlineThickness
+ and post.underlinePosition when generating MVAR deltas (#1449,
+ googlei18n/ufo2ft#308).
+- [subset] Added ``--retain-gids`` option to subset font without modifying the
+ current glyph indices (#1443, #1447).
+- [ufoLib] Replace deprecated calls to ``getbytes`` and ``setbytes`` with new
+ equivalent ``readbytes`` and ``writebytes`` calls. ``fs`` >= 2.2 no required.
+- [varLib] Allow loading masters from TTX files as well (#1441).
+
+3.35.2 (released 2019-01-14)
+----------------------------
+
+- [hmtx/vmtx]: Allow to compile/decompile ``hmtx`` and ``vmtx`` tables even
+ without the corresponding (required) metrics header tables, ``hhea`` and
+ ``vhea`` (#1439).
+- [varLib] Added support for localized axes' ``labelname`` and named instances'
+ ``stylename`` (#1438).
+
+3.35.1 (released 2019-01-09)
+----------------------------
+
+- [_m_a_x_p] Include ``maxComponentElements`` in ``maxp`` table's recalculation.
+
3.35.0 (released 2019-01-07)
----------------------------
diff --git a/PKG-INFO b/PKG-INFO
index 32f95682..28c4c034 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 3.35.0
+Version: 3.37.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -430,6 +430,54 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
Changelog
~~~~~~~~~
+ 3.37.0 (released 2019-01-28)
+ ----------------------------
+
+ - [svgLib] Added support for converting elliptical arcs to cubic bezier curves
+ (#1464).
+ - [py23] Added backport for ``math.isfinite``.
+ - [varLib] Apply HIDDEN flag to fvar axis if designspace axis has attribute
+ ``hidden=1``.
+ - Fixed "DeprecationWarning: invalid escape sequence" in Python 3.7.
+ - [voltLib] Fixed parsing glyph groups. Distinguish different PROCESS_MARKS.
+ Accept COMPONENT glyph type.
+ - [feaLib] Distinguish missing value and explicit ``<NULL>`` for PairPos2
+ format A (#1459). Round-trip ``useExtension`` keyword. Implemented
+ ``ValueRecord.asFea`` method.
+ - [subset] Insert empty widths into hdmx when retaining gids (#1458).
+
+ 3.36.0 (released 2019-01-17)
+ ----------------------------
+
+ - [ttx] Added ``--no-recalc-timestamp`` option to keep the original font's
+ ``head.modified`` timestamp (#1455, #46).
+ - [ttx/psCharStrings] Fixed issues while dumping and round-tripping CFF2 table
+ with ttx (#1451, #1452, #1456).
+ - [voltLib] Fixed check for duplicate anchors (#1450). Don't try to read past
+ the ``END`` operator in .vtp file (#1453).
+ - [varLib] Use sentinel value -0x8000 (-32768) to ignore post.underlineThickness
+ and post.underlinePosition when generating MVAR deltas (#1449,
+ googlei18n/ufo2ft#308).
+ - [subset] Added ``--retain-gids`` option to subset font without modifying the
+ current glyph indices (#1443, #1447).
+ - [ufoLib] Replace deprecated calls to ``getbytes`` and ``setbytes`` with new
+ equivalent ``readbytes`` and ``writebytes`` calls. ``fs`` >= 2.2 no required.
+ - [varLib] Allow loading masters from TTX files as well (#1441).
+
+ 3.35.2 (released 2019-01-14)
+ ----------------------------
+
+ - [hmtx/vmtx]: Allow to compile/decompile ``hmtx`` and ``vmtx`` tables even
+ without the corresponding (required) metrics header tables, ``hhea`` and
+ ``vhea`` (#1439).
+ - [varLib] Added support for localized axes' ``labelname`` and named instances'
+ ``stylename`` (#1438).
+
+ 3.35.1 (released 2019-01-09)
+ ----------------------------
+
+ - [_m_a_x_p] Include ``maxComponentElements`` in ``maxp`` table's recalculation.
+
3.35.0 (released 2019-01-07)
----------------------------
@@ -1582,13 +1630,13 @@ Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
-Provides-Extra: all
-Provides-Extra: interpolatable
-Provides-Extra: woff
-Provides-Extra: lxml
Provides-Extra: unicode
-Provides-Extra: graphite
-Provides-Extra: ufo
-Provides-Extra: type1
+Provides-Extra: interpolatable
Provides-Extra: plot
+Provides-Extra: type1
Provides-Extra: symfont
+Provides-Extra: ufo
+Provides-Extra: all
+Provides-Extra: graphite
+Provides-Extra: lxml
+Provides-Extra: woff
diff --git a/Snippets/cmap-format.py b/Snippets/cmap-format.py
index 0cee39c5..0cee39c5 100644..100755
--- a/Snippets/cmap-format.py
+++ b/Snippets/cmap-format.py
diff --git a/Snippets/interpolate.py b/Snippets/interpolate.py
index 7ed822d2..7ed822d2 100644..100755
--- a/Snippets/interpolate.py
+++ b/Snippets/interpolate.py
diff --git a/Snippets/layout-features.py b/Snippets/layout-features.py
index 25522cda..25522cda 100644..100755
--- a/Snippets/layout-features.py
+++ b/Snippets/layout-features.py
diff --git a/Snippets/otf2ttf.py b/Snippets/otf2ttf.py
index 62b4f735..62b4f735 100644..100755
--- a/Snippets/otf2ttf.py
+++ b/Snippets/otf2ttf.py
diff --git a/Snippets/rename-fonts.py b/Snippets/rename-fonts.py
index ddfce103..ddfce103 100644..100755
--- a/Snippets/rename-fonts.py
+++ b/Snippets/rename-fonts.py
diff --git a/Snippets/subset-fpgm.py b/Snippets/subset-fpgm.py
index c20c05fc..c20c05fc 100644..100755
--- a/Snippets/subset-fpgm.py
+++ b/Snippets/subset-fpgm.py
diff --git a/Snippets/svg2glif.py b/Snippets/svg2glif.py
index 2dd64027..2dd64027 100644..100755
--- a/Snippets/svg2glif.py
+++ b/Snippets/svg2glif.py
diff --git a/Snippets/woff2_compress.py b/Snippets/woff2_compress.py
index 689ebdcc..689ebdcc 100644..100755
--- a/Snippets/woff2_compress.py
+++ b/Snippets/woff2_compress.py
diff --git a/Snippets/woff2_decompress.py b/Snippets/woff2_decompress.py
index e7c1beaa..e7c1beaa 100644..100755
--- a/Snippets/woff2_decompress.py
+++ b/Snippets/woff2_decompress.py
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 18ece3b1..fc1141a8 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -65,7 +65,7 @@ class BuilderTest(unittest.TestCase):
spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g
spec10
bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509
- bug512 bug514 bug568 bug633 bug1307
+ bug512 bug514 bug568 bug633 bug1307 bug1459
name size size2 multiple_feature_blocks omitted_GlyphClassDef
ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical
ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical
diff --git a/Tests/feaLib/data/bug1459.fea b/Tests/feaLib/data/bug1459.fea
new file mode 100644
index 00000000..1ad688e8
--- /dev/null
+++ b/Tests/feaLib/data/bug1459.fea
@@ -0,0 +1,7 @@
+# A pair position lookup where only the second glyph has a non-empty valuerecord
+# while the first glyph has a NULL valuerecord. The ValueFormat1 for the first
+# glyph is expected to be 0.
+# https://github.com/fonttools/fonttools/issues/1459
+feature kern {
+ pos A <NULL> V <-180 0 -90 0>;
+} kern;
diff --git a/Tests/feaLib/data/bug1459.ttx b/Tests/feaLib/data/bug1459.ttx
new file mode 100644
index 00000000..8a7c0962
--- /dev/null
+++ b/Tests/feaLib/data/bug1459.ttx
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="kern"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="2"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <PairPos index="0" Format="1">
+ <Coverage>
+ <Glyph value="A"/>
+ </Coverage>
+ <ValueFormat1 value="0"/>
+ <ValueFormat2 value="5"/>
+ <!-- PairSetCount=1 -->
+ <PairSet index="0">
+ <!-- PairValueCount=1 -->
+ <PairValueRecord index="0">
+ <SecondGlyph value="V"/>
+ <Value2 XPlacement="-180" XAdvance="-90"/>
+ </PairValueRecord>
+ </PairSet>
+ </PairPos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+</ttFont>
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index f9b1063b..6636b413 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -215,6 +215,8 @@ class ParserTest(unittest.TestCase):
[liga] = self.parse("feature liga useExtension {} liga;").statements
self.assertEqual(liga.name, "liga")
self.assertTrue(liga.use_extension)
+ self.assertEqual(liga.asFea(),
+ "feature liga useExtension {\n \n} liga;\n")
def test_feature_comment(self):
[liga] = self.parse("feature liga { # Comment\n } liga;").statements
@@ -608,6 +610,8 @@ class ParserTest(unittest.TestCase):
[lookup] = self.parse("lookup Foo useExtension {} Foo;").statements
self.assertEqual(lookup.name, "Foo")
self.assertTrue(lookup.use_extension)
+ self.assertEqual(lookup.asFea(),
+ "lookup Foo useExtension {\n \n} Foo;\n")
def test_lookup_block_name_mismatch(self):
self.assertRaisesRegex(
@@ -722,7 +726,7 @@ class ParserTest(unittest.TestCase):
self.assertIsInstance(pos, ast.SinglePosStatement)
[(glyphs, value)] = pos.pos
self.assertEqual(glyphstr([glyphs]), "one")
- self.assertEqual(value.makeString(vertical=False), "<1 2 3 4>")
+ self.assertEqual(value.asFea(), "<1 2 3 4>")
def test_gpos_type_1_glyphclass_horizontal(self):
doc = self.parse("feature kern {pos [one two] -300;} kern;")
@@ -730,7 +734,7 @@ class ParserTest(unittest.TestCase):
self.assertIsInstance(pos, ast.SinglePosStatement)
[(glyphs, value)] = pos.pos
self.assertEqual(glyphstr([glyphs]), "[one two]")
- self.assertEqual(value.makeString(vertical=False), "-300")
+ self.assertEqual(value.asFea(), "-300")
def test_gpos_type_1_glyphclass_vertical(self):
doc = self.parse("feature vkrn {pos [one two] -300;} vkrn;")
@@ -738,7 +742,7 @@ class ParserTest(unittest.TestCase):
self.assertIsInstance(pos, ast.SinglePosStatement)
[(glyphs, value)] = pos.pos
self.assertEqual(glyphstr([glyphs]), "[one two]")
- self.assertEqual(value.makeString(vertical=True), "-300")
+ self.assertEqual(value.asFea(), "-300")
def test_gpos_type_1_multiple(self):
doc = self.parse("feature f {pos one'1 two'2 [five six]'56;} f;")
@@ -746,11 +750,11 @@ class ParserTest(unittest.TestCase):
self.assertIsInstance(pos, ast.SinglePosStatement)
[(glyphs1, val1), (glyphs2, val2), (glyphs3, val3)] = pos.pos
self.assertEqual(glyphstr([glyphs1]), "one")
- self.assertEqual(val1.makeString(vertical=False), "1")
+ self.assertEqual(val1.asFea(), "1")
self.assertEqual(glyphstr([glyphs2]), "two")
- self.assertEqual(val2.makeString(vertical=False), "2")
+ self.assertEqual(val2.asFea(), "2")
self.assertEqual(glyphstr([glyphs3]), "[five six]")
- self.assertEqual(val3.makeString(vertical=False), "56")
+ self.assertEqual(val3.asFea(), "56")
self.assertEqual(pos.prefix, [])
self.assertEqual(pos.suffix, [])
@@ -770,7 +774,7 @@ class ParserTest(unittest.TestCase):
self.assertIsInstance(pos, ast.SinglePosStatement)
[(glyphs, value)] = pos.pos
self.assertEqual(glyphstr([glyphs]), "[T Y]")
- self.assertEqual(value.makeString(vertical=False), "20")
+ self.assertEqual(value.asFea(), "20")
self.assertEqual(glyphstr(pos.prefix), "[A B]")
self.assertEqual(glyphstr(pos.suffix), "comma")
@@ -782,10 +786,9 @@ class ParserTest(unittest.TestCase):
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertFalse(pos.enumerated)
self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
- self.assertEqual(pos.valuerecord1.makeString(vertical=False), "-60")
+ self.assertEqual(pos.valuerecord1.asFea(), "-60")
self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
- self.assertEqual(pos.valuerecord2.makeString(vertical=False),
- "<1 2 3 4>")
+ self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>")
def test_gpos_type_2_format_a_enumerated(self):
doc = self.parse("feature kern {"
@@ -795,12 +798,25 @@ class ParserTest(unittest.TestCase):
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertTrue(pos.enumerated)
self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
- self.assertEqual(pos.valuerecord1.makeString(vertical=False), "-60")
+ self.assertEqual(pos.valuerecord1.asFea(), "-60")
self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
- self.assertEqual(pos.valuerecord2.makeString(vertical=False),
- "<1 2 3 4>")
+ self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>")
- def test_gpos_type_2_format_a_with_null(self):
+ def test_gpos_type_2_format_a_with_null_first(self):
+ doc = self.parse("feature kern {"
+ " pos [T V] <NULL> [a b c] <1 2 3 4>;"
+ "} kern;")
+ pos = doc.statements[0].statements[0]
+ self.assertEqual(type(pos), ast.PairPosStatement)
+ self.assertFalse(pos.enumerated)
+ self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
+ self.assertFalse(pos.valuerecord1)
+ self.assertEqual(pos.valuerecord1.asFea(), "<NULL>")
+ self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
+ self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>")
+ self.assertEqual(pos.asFea(), "pos [T V] <NULL> [a b c] <1 2 3 4>;")
+
+ def test_gpos_type_2_format_a_with_null_second(self):
doc = self.parse("feature kern {"
" pos [T V] <1 2 3 4> [a b c] <NULL>;"
"} kern;")
@@ -808,10 +824,10 @@ class ParserTest(unittest.TestCase):
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertFalse(pos.enumerated)
self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
- self.assertEqual(pos.valuerecord1.makeString(vertical=False),
- "<1 2 3 4>")
+ self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>")
self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
- self.assertIsNone(pos.valuerecord2)
+ self.assertFalse(pos.valuerecord2)
+ self.assertEqual(pos.asFea(), "pos [T V] [a b c] <1 2 3 4>;")
def test_gpos_type_2_format_b(self):
doc = self.parse("feature kern {"
@@ -821,8 +837,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertFalse(pos.enumerated)
self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
- self.assertEqual(pos.valuerecord1.makeString(vertical=False),
- "<1 2 3 4>")
+ self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>")
self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
self.assertIsNone(pos.valuerecord2)
@@ -834,8 +849,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertTrue(pos.enumerated)
self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
- self.assertEqual(pos.valuerecord1.makeString(vertical=False),
- "<1 2 3 4>")
+ self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>")
self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
self.assertIsNone(pos.valuerecord2)
@@ -1411,7 +1425,8 @@ class ParserTest(unittest.TestCase):
def test_valuerecord_format_a_horizontal(self):
doc = self.parse("feature liga {valueRecordDef 123 foo;} liga;")
- value = doc.statements[0].statements[0].value
+ valuedef = doc.statements[0].statements[0]
+ value = valuedef.value
self.assertIsNone(value.xPlacement)
self.assertIsNone(value.yPlacement)
self.assertEqual(value.xAdvance, 123)
@@ -1420,11 +1435,13 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(value.yPlaDevice)
self.assertIsNone(value.xAdvDevice)
self.assertIsNone(value.yAdvDevice)
- self.assertEqual(value.makeString(vertical=False), "123")
+ self.assertEqual(valuedef.asFea(), "valueRecordDef 123 foo;")
+ self.assertEqual(value.asFea(), "123")
def test_valuerecord_format_a_vertical(self):
doc = self.parse("feature vkrn {valueRecordDef 123 foo;} vkrn;")
- value = doc.statements[0].statements[0].value
+ valuedef = doc.statements[0].statements[0]
+ value = valuedef.value
self.assertIsNone(value.xPlacement)
self.assertIsNone(value.yPlacement)
self.assertIsNone(value.xAdvance)
@@ -1433,11 +1450,13 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(value.yPlaDevice)
self.assertIsNone(value.xAdvDevice)
self.assertIsNone(value.yAdvDevice)
- self.assertEqual(value.makeString(vertical=True), "123")
+ self.assertEqual(valuedef.asFea(), "valueRecordDef 123 foo;")
+ self.assertEqual(value.asFea(), "123")
def test_valuerecord_format_a_zero_horizontal(self):
doc = self.parse("feature liga {valueRecordDef 0 foo;} liga;")
- value = doc.statements[0].statements[0].value
+ valuedef = doc.statements[0].statements[0]
+ value = valuedef.value
self.assertIsNone(value.xPlacement)
self.assertIsNone(value.yPlacement)
self.assertEqual(value.xAdvance, 0)
@@ -1446,11 +1465,13 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(value.yPlaDevice)
self.assertIsNone(value.xAdvDevice)
self.assertIsNone(value.yAdvDevice)
- self.assertEqual(value.makeString(vertical=False), "0")
+ self.assertEqual(valuedef.asFea(), "valueRecordDef 0 foo;")
+ self.assertEqual(value.asFea(), "0")
def test_valuerecord_format_a_zero_vertical(self):
doc = self.parse("feature vkrn {valueRecordDef 0 foo;} vkrn;")
- value = doc.statements[0].statements[0].value
+ valuedef = doc.statements[0].statements[0]
+ value = valuedef.value
self.assertIsNone(value.xPlacement)
self.assertIsNone(value.yPlacement)
self.assertIsNone(value.xAdvance)
@@ -1459,7 +1480,8 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(value.yPlaDevice)
self.assertIsNone(value.xAdvDevice)
self.assertIsNone(value.yAdvDevice)
- self.assertEqual(value.makeString(vertical=True), "0")
+ self.assertEqual(valuedef.asFea(), "valueRecordDef 0 foo;")
+ self.assertEqual(value.asFea(), "0")
def test_valuerecord_format_a_vertical_contexts_(self):
for tag in "vkrn vpal vhal valt".split():
@@ -1472,7 +1494,8 @@ class ParserTest(unittest.TestCase):
def test_valuerecord_format_b(self):
doc = self.parse("feature liga {valueRecordDef <1 2 3 4> foo;} liga;")
- value = doc.statements[0].statements[0].value
+ valuedef = doc.statements[0].statements[0]
+ value = valuedef.value
self.assertEqual(value.xPlacement, 1)
self.assertEqual(value.yPlacement, 2)
self.assertEqual(value.xAdvance, 3)
@@ -1481,11 +1504,13 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(value.yPlaDevice)
self.assertIsNone(value.xAdvDevice)
self.assertIsNone(value.yAdvDevice)
- self.assertEqual(value.makeString(vertical=False), "<1 2 3 4>")
+ self.assertEqual(valuedef.asFea(), "valueRecordDef <1 2 3 4> foo;")
+ self.assertEqual(value.asFea(), "<1 2 3 4>")
def test_valuerecord_format_b_zero(self):
doc = self.parse("feature liga {valueRecordDef <0 0 0 0> foo;} liga;")
- value = doc.statements[0].statements[0].value
+ valuedef = doc.statements[0].statements[0]
+ value = valuedef.value
self.assertEqual(value.xPlacement, 0)
self.assertEqual(value.yPlacement, 0)
self.assertEqual(value.xAdvance, 0)
@@ -1494,7 +1519,8 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(value.yPlaDevice)
self.assertIsNone(value.xAdvDevice)
self.assertIsNone(value.yAdvDevice)
- self.assertEqual(value.makeString(vertical=False), "<0 0 0 0>")
+ self.assertEqual(valuedef.asFea(), "valueRecordDef <0 0 0 0> foo;")
+ self.assertEqual(value.asFea(), "<0 0 0 0>")
def test_valuerecord_format_c(self):
doc = self.parse(
@@ -1516,14 +1542,15 @@ class ParserTest(unittest.TestCase):
self.assertEqual(value.yPlaDevice, ((11, 111), (12, 112)))
self.assertIsNone(value.xAdvDevice)
self.assertEqual(value.yAdvDevice, ((33, -113), (44, -114), (55, 115)))
- self.assertEqual(value.makeString(vertical=False),
+ self.assertEqual(value.asFea(),
"<1 2 3 4 <device 8 88> <device 11 111, 12 112>"
" <device NULL> <device 33 -113, 44 -114, 55 115>>")
def test_valuerecord_format_d(self):
doc = self.parse("feature test {valueRecordDef <NULL> foo;} test;")
value = doc.statements[0].statements[0].value
- self.assertIsNone(value)
+ self.assertFalse(value)
+ self.assertEqual(value.asFea(), "<NULL>")
def test_valuerecord_named(self):
doc = self.parse("valueRecordDef <1 2 3 4> foo;"
diff --git a/Tests/fontBuilder/data/test_uvs.ttf.ttx b/Tests/fontBuilder/data/test_uvs.ttf.ttx
new file mode 100644
index 00000000..55b0a807
--- /dev/null
+++ b/Tests/fontBuilder/data/test_uvs.ttf.ttx
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.35">
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x20" name="space"/><!-- SPACE -->
+ <map code="0x30" name="zero"/><!-- DIGIT ZERO -->
+ </cmap_format_4>
+ <cmap_format_14 platformID="0" platEncID="5">
+ <map uv="0x30" uvs="0xfe00" name="zero.slash"/>
+ <map uv="0x30" uvs="0xfe01"/>
+ </cmap_format_14>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x20" name="space"/><!-- SPACE -->
+ <map code="0x30" name="zero"/><!-- DIGIT ZERO -->
+ </cmap_format_4>
+ </cmap>
+
+</ttFont>
diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py
index d23f0f17..97d7cb6a 100644
--- a/Tests/fontBuilder/fontBuilder_test.py
+++ b/Tests/fontBuilder/fontBuilder_test.py
@@ -53,9 +53,9 @@ def _setupFontBuilder(isTTF, unitsPerEm=1024):
return fb, advanceWidths, nameStrings
-def _verifyOutput(outPath):
+def _verifyOutput(outPath, tables=None):
f = TTFont(outPath)
- f.saveXML(outPath + ".ttx")
+ f.saveXML(outPath + ".ttx", tables=tables)
with open(outPath + ".ttx") as f:
testData = strip_VariableItems(f.read())
refData = strip_VariableItems(getTestData(os.path.basename(outPath) + ".ttx"))
@@ -244,3 +244,42 @@ def test_setupNameTable_no_windows():
assert all(n for n in fb.font["name"].names if n.platformID == 1)
assert not any(n for n in fb.font["name"].names if n.platformID == 3)
+
+
+def test_unicodeVariationSequences(tmpdir):
+ familyName = "UVSTestFont"
+ styleName = "Regular"
+ nameStrings = dict(familyName=familyName, styleName=styleName)
+ nameStrings['psName'] = familyName + "-" + styleName
+ glyphOrder = [".notdef", "space", "zero", "zero.slash"]
+ cmap = {ord(" "): "space", ord("0"): "zero"}
+ uvs = [
+ (0x0030, 0xFE00, "zero.slash"),
+ (0x0030, 0xFE01, None), # not an official sequence, just testing
+ ]
+ metrics = {gn: (600, 0) for gn in glyphOrder}
+ pen = TTGlyphPen(None)
+ glyph = pen.glyph() # empty placeholder
+ glyphs = {gn: glyph for gn in glyphOrder}
+
+ fb = FontBuilder(1024, isTTF=True)
+ fb.setupGlyphOrder(glyphOrder)
+ fb.setupCharacterMap(cmap, uvs)
+ fb.setupGlyf(glyphs)
+ fb.setupHorizontalMetrics(metrics)
+ fb.setupHorizontalHeader(ascent=824, descent=200)
+ fb.setupNameTable(nameStrings)
+ fb.setupOS2()
+ fb.setupPost()
+
+ outPath = os.path.join(str(tmpdir), "test_uvs.ttf")
+ fb.save(outPath)
+ _verifyOutput(outPath, tables=["cmap"])
+
+ uvs = [
+ (0x0030, 0xFE00, "zero.slash"),
+ (0x0030, 0xFE01, "zero"), # should result in the exact same subtable data, due to cmap[0x0030] == "zero"
+ ]
+ fb.setupCharacterMap(cmap, uvs)
+ fb.save(outPath)
+ _verifyOutput(outPath, tables=["cmap"])
diff --git a/Tests/misc/loggingTools_test.py b/Tests/misc/loggingTools_test.py
index 694c1090..b402efa5 100644
--- a/Tests/misc/loggingTools_test.py
+++ b/Tests/misc/loggingTools_test.py
@@ -122,13 +122,13 @@ class TimerTest(object):
test1()
assert re.match(
- "Took [0-9]\.[0-9]{3}s to run 'test1'",
+ r"Took [0-9]\.[0-9]{3}s to run 'test1'",
logger.handlers[0].stream.getvalue())
test2()
assert re.search(
- "Took [0-9]\.[0-9]{3}s to run test 2",
+ r"Took [0-9]\.[0-9]{3}s to run test 2",
logger.handlers[0].stream.getvalue())
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
index 76d89c22..72fee41b 100644
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -475,6 +475,107 @@ class SubsetTest(unittest.TestCase):
subset.main([fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"])
self.assertLess(modified, TTFont(subsetpath)['head'].modified)
+ def test_retain_gids_ttf(self):
+ _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
+ font = TTFont(fontpath)
+
+ self.assertEqual(font["hmtx"]["A"], (500, 132))
+ self.assertEqual(font["hmtx"]["B"], (400, 132))
+
+ self.assertGreater(font["glyf"]["A"].numberOfContours, 0)
+ self.assertGreater(font["glyf"]["B"].numberOfContours, 0)
+
+ subsetpath = self.temp_path(".ttf")
+ subset.main(
+ [
+ fontpath,
+ "--retain-gids",
+ "--output-file=%s" % subsetpath,
+ "--glyph-names",
+ "A",
+ ]
+ )
+ subsetfont = TTFont(subsetpath)
+
+ self.assertEqual(subsetfont.getGlyphOrder(), font.getGlyphOrder())
+
+ hmtx = subsetfont["hmtx"]
+ self.assertEqual(hmtx["A"], (500, 132))
+ self.assertEqual(hmtx["B"], (0, 0))
+
+ glyf = subsetfont["glyf"]
+ self.assertGreater(glyf["A"].numberOfContours, 0)
+ self.assertEqual(glyf["B"].numberOfContours, 0)
+
+ def test_retain_gids_cff(self):
+ _, fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf")
+ font = TTFont(fontpath)
+
+ self.assertEqual(font["hmtx"]["A"], (500, 132))
+ self.assertEqual(font["hmtx"]["B"], (400, 132))
+
+ font["CFF "].cff[0].decompileAllCharStrings()
+ cs = font["CFF "].cff[0].CharStrings
+ self.assertGreater(len(cs["A"].program), 0)
+ self.assertGreater(len(cs["B"].program), 0)
+
+ subsetpath = self.temp_path(".otf")
+ subset.main(
+ [
+ fontpath,
+ "--retain-gids",
+ "--output-file=%s" % subsetpath,
+ "--glyph-names",
+ "A",
+ ]
+ )
+ subsetfont = TTFont(subsetpath)
+
+ self.assertEqual(subsetfont.getGlyphOrder(), font.getGlyphOrder())
+
+ hmtx = subsetfont["hmtx"]
+ self.assertEqual(hmtx["A"], (500, 132))
+ self.assertEqual(hmtx["B"], (0, 0))
+
+ subsetfont["CFF "].cff[0].decompileAllCharStrings()
+ cs = subsetfont["CFF "].cff[0].CharStrings
+ self.assertGreater(len(cs["A"].program), 0)
+ self.assertEqual(cs["B"].program, ["endchar"])
+
+ def test_retain_gids_cff2(self):
+ fontpath = self.getpath("../../varLib/data/TestCFF2VF.otf")
+ font = TTFont(fontpath)
+
+ self.assertEqual(font["hmtx"]["A"], (600, 31))
+ self.assertEqual(font["hmtx"]["T"], (600, 41))
+
+ font["CFF2"].cff[0].decompileAllCharStrings()
+ cs = font["CFF2"].cff[0].CharStrings
+ self.assertGreater(len(cs["A"].program), 0)
+ self.assertGreater(len(cs["T"].program), 0)
+
+ subsetpath = self.temp_path(".otf")
+ subset.main(
+ [
+ fontpath,
+ "--retain-gids",
+ "--output-file=%s" % subsetpath,
+ "A",
+ ]
+ )
+ subsetfont = TTFont(subsetpath)
+
+ self.assertEqual(len(subsetfont.getGlyphOrder()), len(font.getGlyphOrder()))
+
+ hmtx = subsetfont["hmtx"]
+ self.assertEqual(hmtx["A"], (600, 31))
+ self.assertEqual(hmtx["glyph00002"], (0, 0))
+
+ subsetfont["CFF2"].cff[0].decompileAllCharStrings()
+ cs = subsetfont["CFF2"].cff[0].CharStrings
+ self.assertGreater(len(cs["A"].program), 0)
+ self.assertEqual(cs["glyph00002"].program, [])
+
if __name__ == "__main__":
sys.exit(unittest.main())
diff --git a/Tests/svgLib/path/parser_test.py b/Tests/svgLib/path/parser_test.py
index 78658700..eada14e7 100644
--- a/Tests/svgLib/path/parser_test.py
+++ b/Tests/svgLib/path/parser_test.py
@@ -290,8 +290,67 @@ def test_invalid_implicit_command():
assert exc_info.match("Unallowed implicit command")
-def test_arc_not_implemented():
- pathdef = "M300,200 h-150 a150,150 0 1,0 150,-150 z"
- with pytest.raises(NotImplementedError) as exc_info:
- parse_path(pathdef, RecordingPen())
- assert exc_info.match("arcs are not supported")
+def test_arc_to_cubic_bezier():
+ pen = RecordingPen()
+ parse_path("M300,200 h-150 a150,150 0 1,0 150,-150 z", pen)
+ expected = [
+ ('moveTo', ((300.0, 200.0),)),
+ ('lineTo', ((150.0, 200.0),)),
+ (
+ 'curveTo',
+ (
+ (150.0, 282.842),
+ (217.157, 350.0),
+ (300.0, 350.0)
+ )
+ ),
+ (
+ 'curveTo',
+ (
+ (382.842, 350.0),
+ (450.0, 282.842),
+ (450.0, 200.0)
+ )
+ ),
+ (
+ 'curveTo',
+ (
+ (450.0, 117.157),
+ (382.842, 50.0),
+ (300.0, 50.0)
+ )
+ ),
+ ('lineTo', ((300.0, 200.0),)),
+ ('closePath', ())
+ ]
+
+ result = list(pen.value)
+ assert len(result) == len(expected)
+ for (cmd1, points1), (cmd2, points2) in zip(result, expected):
+ assert cmd1 == cmd2
+ assert len(points1) == len(points2)
+ for pt1, pt2 in zip(points1, points2):
+ assert pt1 == pytest.approx(pt2, rel=1e-5)
+
+
+
+class ArcRecordingPen(RecordingPen):
+
+ def arcTo(self, rx, ry, rotation, arc_large, arc_sweep, end_point):
+ self.value.append(
+ ("arcTo", (rx, ry, rotation, arc_large, arc_sweep, end_point))
+ )
+
+
+def test_arc_pen_with_arcTo():
+ pen = ArcRecordingPen()
+ parse_path("M300,200 h-150 a150,150 0 1,0 150,-150 z", pen)
+ expected = [
+ ('moveTo', ((300.0, 200.0),)),
+ ('lineTo', ((150.0, 200.0),)),
+ ('arcTo', (150.0, 150.0, 0.0, True, False, (300.0, 50.0))),
+ ('lineTo', ((300.0, 200.0),)),
+ ('closePath', ())
+ ]
+
+ assert pen.value == expected
diff --git a/Tests/ttLib/tables/_c_m_a_p_test.py b/Tests/ttLib/tables/_c_m_a_p_test.py
index 66473458..306d0489 100644
--- a/Tests/ttLib/tables/_c_m_a_p_test.py
+++ b/Tests/ttLib/tables/_c_m_a_p_test.py
@@ -1,9 +1,23 @@
from __future__ import print_function, division, absolute_import, unicode_literals
+import io
+import os
+import re
from fontTools.misc.py23 import *
from fontTools import ttLib
+from fontTools.fontBuilder import FontBuilder
import unittest
from fontTools.ttLib.tables._c_m_a_p import CmapSubtable, table__c_m_a_p
+CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+DATA_DIR = os.path.join(CURR_DIR, 'data')
+CMAP_FORMAT_14_TTX = os.path.join(DATA_DIR, "_c_m_a_p_format_14.ttx")
+CMAP_FORMAT_14_BW_COMPAT_TTX = os.path.join(DATA_DIR, "_c_m_a_p_format_14_bw_compat.ttx")
+
+def strip_VariableItems(string):
+ # ttlib changes with the fontTools version
+ string = re.sub(' ttLibVersion=".*"', '', string)
+ return string
+
class CmapSubtableTest(unittest.TestCase):
def makeSubtable(self, cmapFormat, platformID, platEncID, langID):
@@ -82,6 +96,37 @@ class CmapSubtableTest(unittest.TestCase):
self.assertEqual(font.getBestCmap(cmapPreferences=[(3, 1)]), {0x0041:'A', 0x0391:'A'})
self.assertEqual(font.getBestCmap(cmapPreferences=[(0, 4)]), None)
+ def test_format_14(self):
+ subtable = self.makeSubtable(14, 0, 5, 0)
+ subtable.cmap = {} # dummy
+ subtable.uvsDict = {
+ 0xFE00: [(0x0030, "zero.slash")],
+ 0xFE01: [(0x0030, None)],
+ }
+ fb = FontBuilder(1024, isTTF=True)
+ font = fb.font
+ fb.setupGlyphOrder([".notdef", "zero.slash"])
+ fb.setupMaxp()
+ fb.setupPost()
+ cmap = table__c_m_a_p()
+ cmap.tableVersion = 0
+ cmap.tables = [subtable]
+ font["cmap"] = cmap
+ f = io.BytesIO()
+ font.save(f)
+ f.seek(0)
+ font = ttLib.TTFont(f)
+ self.assertEqual(font["cmap"].getcmap(0, 5).uvsDict, subtable.uvsDict)
+ f = io.StringIO(newline=None)
+ font.saveXML(f, tables=["cmap"])
+ ttx = strip_VariableItems(f.getvalue())
+ with open(CMAP_FORMAT_14_TTX) as f:
+ expected = strip_VariableItems(f.read())
+ self.assertEqual(ttx, expected)
+ with open(CMAP_FORMAT_14_BW_COMPAT_TTX) as f:
+ font.importXML(f)
+ self.assertEqual(font["cmap"].getcmap(0, 5).uvsDict, subtable.uvsDict)
+
if __name__ == "__main__":
import sys
diff --git a/Tests/ttLib/tables/_h_m_t_x_test.py b/Tests/ttLib/tables/_h_m_t_x_test.py
index 007f3cd5..5c79fb88 100644
--- a/Tests/ttLib/tables/_h_m_t_x_test.py
+++ b/Tests/ttLib/tables/_h_m_t_x_test.py
@@ -107,6 +107,27 @@ class HmtxTableTest(unittest.TestCase):
len([r for r in captor.records
if "has a huge advance" in r.msg]) == 1)
+ def test_decompile_no_header_table(self):
+ font = TTFont()
+ maxp = font['maxp'] = newTable('maxp')
+ maxp.numGlyphs = 3
+ font.glyphOrder = ["A", "B", "C"]
+
+ self.assertNotIn(self.tableClass.headerTag, font)
+
+ data = deHexStr("0190 001E 0190 0028 0190 0032")
+ mtxTable = newTable(self.tag)
+ mtxTable.decompile(data, font)
+
+ self.assertEqual(
+ mtxTable.metrics,
+ {
+ "A": (400, 30),
+ "B": (400, 40),
+ "C": (400, 50),
+ }
+ )
+
def test_compile(self):
# we set the wrong 'numberOfMetrics' to check it gets adjusted
font = self.makeFont(numGlyphs=3, numberOfMetrics=4)
@@ -173,6 +194,24 @@ class HmtxTableTest(unittest.TestCase):
self.assertEqual(data, deHexStr("0001 0001 0000 0001 0000"))
+ def test_compile_no_header_table(self):
+ font = TTFont()
+ maxp = font['maxp'] = newTable('maxp')
+ maxp.numGlyphs = 3
+ font.glyphOrder = [chr(i) for i in range(65, 68)]
+ mtxTable = font[self.tag] = newTable(self.tag)
+ mtxTable.metrics = {
+ "A": (400, 30),
+ "B": (400, 40),
+ "C": (400, 50),
+ }
+
+ self.assertNotIn(self.tableClass.headerTag, font)
+
+ data = mtxTable.compile(font)
+
+ self.assertEqual(data, deHexStr("0190 001E 0190 0028 0190 0032"))
+
def test_toXML(self):
font = self.makeFont(numGlyphs=2, numberOfMetrics=2)
mtxTable = font[self.tag] = newTable(self.tag)
diff --git a/Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx b/Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx
new file mode 100644
index 00000000..73bc6bf9
--- /dev/null
+++ b/Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.35">
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_14 platformID="0" platEncID="5">
+ <map uv="0x30" uvs="0xfe00" name="zero.slash"/>
+ <map uv="0x30" uvs="0xfe01"/>
+ </cmap_format_14>
+ </cmap>
+
+</ttFont>
diff --git a/Tests/ttLib/tables/data/_c_m_a_p_format_14_bw_compat.ttx b/Tests/ttLib/tables/data/_c_m_a_p_format_14_bw_compat.ttx
new file mode 100644
index 00000000..00be8cae
--- /dev/null
+++ b/Tests/ttLib/tables/data/_c_m_a_p_format_14_bw_compat.ttx
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.35">
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_14 platformID="0" platEncID="5">
+ <map uvs="0xfe00" uv="0x30" name="zero.slash"/>
+ <map uvs="0xfe01" uv="0x30" name="None"/><!-- testing whether the old format that uses name="None" is still accepted -->
+ </cmap_format_14>
+ </cmap>
+
+</ttFont>
diff --git a/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap
index ced8c8ae..acead7ba 100644
--- a/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap
+++ b/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap
@@ -3,14 +3,14 @@
<cmap>
<tableVersion version="0"/>
- <cmap_format_14 platformID="0" platEncID="5" format="14" length="47" numVarSelectorRecords="1">
- <map uvs="0xe0100" uv="0x4e00" name="None"/>
- <map uvs="0xe0100" uv="0x4e03" name="None"/>
- <map uvs="0xe0100" uv="0x4e04" name="None"/>
- <map uvs="0xe0100" uv="0x4e05" name="None"/>
- <map uvs="0xe0100" uv="0x4e06" name="None"/>
- <map uvs="0xe0100" uv="0x4e10" name="g25"/>
- <map uvs="0xe0100" uv="0x4e11" name="g26"/>
+ <cmap_format_14 platformID="0" platEncID="5">
+ <map uv="0x4e00" uvs="0xe0100"/>
+ <map uv="0x4e03" uvs="0xe0100"/>
+ <map uv="0x4e04" uvs="0xe0100"/>
+ <map uv="0x4e05" uvs="0xe0100"/>
+ <map uv="0x4e06" uvs="0xe0100"/>
+ <map uv="0x4e10" uvs="0xe0100" name="g25"/>
+ <map uv="0x4e11" uvs="0xe0100" name="g26"/>
</cmap_format_14>
<cmap_format_4 platformID="3" platEncID="1" language="0">
<map code="0x4e00" name="g10"/><!-- CJK UNIFIED IDEOGRAPH-4E00 -->
diff --git a/Tests/ttLib/woff2_test.py b/Tests/ttLib/woff2_test.py
index b3ec6b21..55c4b778 100644
--- a/Tests/ttLib/woff2_test.py
+++ b/Tests/ttLib/woff2_test.py
@@ -765,7 +765,7 @@ class Base128Test(unittest.TestCase):
self.assertRaisesRegex(
ttLib.TTLibError,
- "UIntBase128 value exceeds 2\*\*32-1",
+ r"UIntBase128 value exceeds 2\*\*32-1",
unpackBase128, b'\x90\x80\x80\x80\x00')
self.assertRaisesRegex(
@@ -783,11 +783,11 @@ class Base128Test(unittest.TestCase):
self.assertEqual(packBase128(2**32-1), b'\x8f\xff\xff\xff\x7f')
self.assertRaisesRegex(
ttLib.TTLibError,
- "UIntBase128 format requires 0 <= integer <= 2\*\*32-1",
+ r"UIntBase128 format requires 0 <= integer <= 2\*\*32-1",
packBase128, 2**32+1)
self.assertRaisesRegex(
ttLib.TTLibError,
- "UIntBase128 format requires 0 <= integer <= 2\*\*32-1",
+ r"UIntBase128 format requires 0 <= integer <= 2\*\*32-1",
packBase128, -1)
diff --git a/Tests/ttx/ttx_test.py b/Tests/ttx/ttx_test.py
index ceb7abd3..eb5816ba 100644
--- a/Tests/ttx/ttx_test.py
+++ b/Tests/ttx/ttx_test.py
@@ -476,6 +476,11 @@ def test_options_recalc_timestamp():
assert tto.recalcTimestamp is True
+def test_options_recalc_timestamp():
+ tto = ttx.Options([("--no-recalc-timestamp", "")], 1)
+ assert tto.recalcTimestamp is False
+
+
def test_options_flavor():
tto = ttx.Options([("--flavor", "woff")], 1)
assert tto.flavor == "woff"
@@ -789,6 +794,14 @@ def test_ttcompile_timestamp_calcs(inpath, outpath1, outpath2, tmpdir):
ttf = TTFont(str(outttf2))
assert ttf["head"].modified > epochtime
+ # --no-recalc-timestamp will keep original timestamp
+ options.recalcTimestamp = False
+ ttx.ttCompile(inttx, str(outttf2), options)
+ assert outttf2.check(file=True)
+ inttf = TTFont()
+ inttf.importXML(inttx)
+ assert inttf["head"].modified == TTFont(str(outttf2))["head"].modified
+
# -------------------------
# ttx.ttList function tests
diff --git a/Tests/ufoLib/testSupport.py b/Tests/ufoLib/testSupport.py
index 2982ce84..2982ce84 100644..100755
--- a/Tests/ufoLib/testSupport.py
+++ b/Tests/ufoLib/testSupport.py
diff --git a/Tests/varLib/data/Build.designspace b/Tests/varLib/data/Build.designspace
index e0bf58de..29a43ce9 100644
--- a/Tests/varLib/data/Build.designspace
+++ b/Tests/varLib/data/Build.designspace
@@ -4,6 +4,8 @@
<axis default="368.0" maximum="1000.0" minimum="0.0" name="weight" tag="wght" />
<axis default="0.0" maximum="100.0" minimum="0.0" name="contrast" tag="cntr">
<labelname xml:lang="en">Contrast</labelname>
+ <labelname xml:lang="de">Kontrast</labelname>
+ <labelname xml:lang="fa">کنتراست</labelname>
</axis>
</axes>
<sources>
diff --git a/Tests/varLib/data/SparseMasters.designspace b/Tests/varLib/data/SparseMasters.designspace
new file mode 100644
index 00000000..327fb43b
--- /dev/null
+++ b/Tests/varLib/data/SparseMasters.designspace
@@ -0,0 +1,23 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="350" maximum="625" default="350"/>
+ </axes>
+ <sources>
+ <source filename="master_ttx_interpolatable_ttf/SparseMasters-Regular.ttx" name="Sparse Masters Regular">
+ <location>
+ <dimension name="Weight" xvalue="350"/>
+ </location>
+ </source>
+ <source filename="master_ttx_interpolatable_ttf/SparseMasters-Medium.ttx" name="Sparse Masters Medium">
+ <location>
+ <dimension name="Weight" xvalue="450"/>
+ </location>
+ </source>
+ <source filename="master_ttx_interpolatable_ttf/SparseMasters-Bold.ttx" name="Sparse Masters Bold">
+ <location>
+ <dimension name="Weight" xvalue="625"/>
+ </location>
+ </source>
+ </sources>
+</designspace>
diff --git a/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx b/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx
index 5360fe40..5360fe40 100644..100755
--- a/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx
+++ b/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx
diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Bold.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Bold.ttx
new file mode 100644
index 00000000..55d686e3
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Bold.ttx
@@ -0,0 +1,419 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.35">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="a"/>
+ <GlyphID id="2" name="e"/>
+ <GlyphID id="3" name="s"/>
+ <GlyphID id="4" name="dotabovecomb"/>
+ <GlyphID id="5" name="edotabove"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="0.0"/>
+ <checkSumAdjustment value="0x778ee739"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Wed Nov 21 11:49:03 2018"/>
+ <modified value="Tue Jan 15 18:40:09 2019"/>
+ <xMin value="-64"/>
+ <yMin value="-250"/>
+ <xMax value="608"/>
+ <yMax value="812"/>
+ <macStyle value="00000000 00000001"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="950"/>
+ <descent value="-250"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="600"/>
+ <minLeftSideBearing value="-64"/>
+ <minRightSideBearing value="-63"/>
+ <xMaxExtent value="608"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="6"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="6"/>
+ <maxPoints value="18"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="17"/>
+ <maxCompositeContours value="2"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="2"/>
+ <maxComponentDepth value="1"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="580"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="650"/>
+ <ySubscriptYSize value="600"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="75"/>
+ <ySuperscriptXSize value="650"/>
+ <ySuperscriptYSize value="600"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="350"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="300"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 01000101"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 00100000"/>
+ <usFirstCharIndex value="97"/>
+ <usLastCharIndex value="775"/>
+ <sTypoAscender value="750"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="200"/>
+ <usWinAscent value="950"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="500"/>
+ <sCapHeight value="700"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="4"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="500" lsb="50"/>
+ <mtx name="a" width="600" lsb="9"/>
+ <mtx name="dotabovecomb" width="0" lsb="-64"/>
+ <mtx name="e" width="600" lsb="9"/>
+ <mtx name="edotabove" width="600" lsb="9"/>
+ <mtx name="s" width="600" lsb="7"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x65" name="e"/><!-- LATIN SMALL LETTER E -->
+ <map code="0x73" name="s"/><!-- LATIN SMALL LETTER S -->
+ <map code="0x117" name="edotabove"/><!-- LATIN SMALL LETTER E WITH DOT ABOVE -->
+ <map code="0x307" name="dotabovecomb"/><!-- COMBINING DOT ABOVE -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x65" name="e"/><!-- LATIN SMALL LETTER E -->
+ <map code="0x73" name="s"/><!-- LATIN SMALL LETTER S -->
+ <map code="0x117" name="edotabove"/><!-- LATIN SMALL LETTER E WITH DOT ABOVE -->
+ <map code="0x307" name="dotabovecomb"/><!-- COMBINING DOT ABOVE -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="50" yMin="-250" xMax="450" yMax="750">
+ <contour>
+ <pt x="50" y="-250" on="1"/>
+ <pt x="450" y="-250" on="1"/>
+ <pt x="450" y="750" on="1"/>
+ <pt x="50" y="750" on="1"/>
+ </contour>
+ <contour>
+ <pt x="100" y="-200" on="1"/>
+ <pt x="100" y="700" on="1"/>
+ <pt x="400" y="700" on="1"/>
+ <pt x="400" y="-200" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="a" xMin="9" yMin="-12" xMax="468" yMax="504">
+ <contour>
+ <pt x="468" y="-1" on="1"/>
+ <pt x="307" y="-1" on="1"/>
+ <pt x="304" y="303" on="1"/>
+ <pt x="208" y="341" on="1"/>
+ <pt x="36" y="281" on="1"/>
+ <pt x="9" y="428" on="1"/>
+ <pt x="214" y="504" on="1"/>
+ <pt x="447" y="434" on="1"/>
+ </contour>
+ <contour>
+ <pt x="378" y="263" on="1"/>
+ <pt x="381" y="184" on="1"/>
+ <pt x="165" y="179" on="1"/>
+ <pt x="163" y="133" on="1"/>
+ <pt x="201" y="102" on="1"/>
+ <pt x="383" y="149" on="1"/>
+ <pt x="389" y="71" on="1"/>
+ <pt x="168" y="-12" on="1"/>
+ <pt x="29" y="22" on="1"/>
+ <pt x="26" y="240" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="dotabovecomb" xMin="-64" yMin="483" xMax="63" yMax="625">
+ <contour>
+ <pt x="-29" y="625" on="1"/>
+ <pt x="63" y="605" on="1"/>
+ <pt x="58" y="488" on="1"/>
+ <pt x="-64" y="483" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="e" xMin="9" yMin="-18" xMax="601" yMax="548">
+ <contour>
+ <pt x="197" y="229" on="1"/>
+ <pt x="195" y="299" on="1"/>
+ <pt x="404" y="293" on="1"/>
+ <pt x="301" y="360" on="1"/>
+ <pt x="217" y="264" on="1"/>
+ <pt x="244" y="130" on="1"/>
+ <pt x="524" y="184" on="1"/>
+ <pt x="528" y="0" on="1"/>
+ <pt x="188" y="-18" on="1"/>
+ <pt x="9" y="262" on="1"/>
+ <pt x="314" y="548" on="1"/>
+ <pt x="596" y="304" on="1"/>
+ <pt x="601" y="225" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="edotabove" xMin="9" yMin="-18" xMax="601" yMax="812">
+ <component glyphName="e" x="0" y="0" flags="0x204"/>
+ <component glyphName="dotabovecomb" x="307" y="187" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="s" xMin="7" yMin="-58" xMax="608" yMax="530">
+ <contour>
+ <pt x="559" y="459" on="1"/>
+ <pt x="537" y="336" on="1"/>
+ <pt x="324" y="402" on="1"/>
+ <pt x="268" y="357" on="1"/>
+ <pt x="608" y="141" on="1"/>
+ <pt x="284" y="-58" on="1"/>
+ <pt x="7" y="79" on="1"/>
+ <pt x="26" y="226" on="1"/>
+ <pt x="221" y="119" on="1"/>
+ <pt x="347" y="149" on="1"/>
+ <pt x="16" y="398" on="1"/>
+ <pt x="324" y="530" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Layer Font
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Bold
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 0.000;NONE;LayerFont-Bold
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Layer Font Bold
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 0.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ LayerFont-Bold
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-100"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ <psName name="dotabovecomb"/>
+ <psName name="edotabove"/>
+ </extraNames>
+ </post>
+
+ <GDEF>
+ <Version value="0x00010000"/>
+ <GlyphClassDef Format="1">
+ <ClassDef glyph="dotabovecomb" class="3"/>
+ <ClassDef glyph="e" class="1"/>
+ </GlyphClassDef>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="mark"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <MarkBasePos index="0" Format="1">
+ <MarkCoverage Format="1">
+ <Glyph value="dotabovecomb"/>
+ </MarkCoverage>
+ <BaseCoverage Format="1">
+ <Glyph value="e"/>
+ </BaseCoverage>
+ <!-- ClassCount=1 -->
+ <MarkArray>
+ <!-- MarkCount=1 -->
+ <MarkRecord index="0">
+ <Class value="0"/>
+ <MarkAnchor Format="1">
+ <XCoordinate value="-2"/>
+ <YCoordinate value="465"/>
+ </MarkAnchor>
+ </MarkRecord>
+ </MarkArray>
+ <BaseArray>
+ <!-- BaseCount=1 -->
+ <BaseRecord index="0">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="315"/>
+ <YCoordinate value="644"/>
+ </BaseAnchor>
+ </BaseRecord>
+ </BaseArray>
+ </MarkBasePos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <LigatureSubst index="0" Format="1">
+ <LigatureSet glyph="a">
+ <Ligature components="e,s,s" glyph="s"/>
+ </LigatureSet>
+ </LigatureSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Medium.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Medium.ttx
new file mode 100644
index 00000000..eb3c7454
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Medium.ttx
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.35">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="e"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="0.0"/>
+ <checkSumAdjustment value="0x62ddba7b"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Wed Nov 21 11:49:03 2018"/>
+ <modified value="Tue Jan 15 18:40:09 2019"/>
+ <xMin value="40"/>
+ <yMin value="-250"/>
+ <xMax value="576"/>
+ <yMax value="750"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="2"/>
+ <maxPoints value="13"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <hmtx>
+ <mtx name=".notdef" width="500" lsb="50"/>
+ <mtx name="e" width="600" lsb="40"/>
+ </hmtx>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="50" yMin="-250" xMax="450" yMax="750">
+ <contour>
+ <pt x="50" y="-250" on="1"/>
+ <pt x="450" y="-250" on="1"/>
+ <pt x="450" y="750" on="1"/>
+ <pt x="50" y="750" on="1"/>
+ </contour>
+ <contour>
+ <pt x="100" y="-200" on="1"/>
+ <pt x="100" y="700" on="1"/>
+ <pt x="400" y="700" on="1"/>
+ <pt x="400" y="-200" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="e" xMin="40" yMin="-18" xMax="576" yMax="513">
+ <contour>
+ <pt x="126" y="203" on="1"/>
+ <pt x="125" y="298" on="1"/>
+ <pt x="396" y="297" on="1"/>
+ <pt x="318" y="387" on="1"/>
+ <pt x="180" y="264" on="1"/>
+ <pt x="264" y="116" on="1"/>
+ <pt x="507" y="157" on="1"/>
+ <pt x="526" y="45" on="1"/>
+ <pt x="188" y="-18" on="1"/>
+ <pt x="40" y="261" on="1"/>
+ <pt x="316" y="513" on="1"/>
+ <pt x="571" y="305" on="1"/>
+ <pt x="576" y="199" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-32768"/>
+ <underlineThickness value="-32768"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ </extraNames>
+ </post>
+
+</ttFont>
diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Regular.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Regular.ttx
new file mode 100644
index 00000000..e013e0b7
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/SparseMasters-Regular.ttx
@@ -0,0 +1,419 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.35">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="a"/>
+ <GlyphID id="2" name="e"/>
+ <GlyphID id="3" name="s"/>
+ <GlyphID id="4" name="dotabovecomb"/>
+ <GlyphID id="5" name="edotabove"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="0.0"/>
+ <checkSumAdjustment value="0x5cdfc4c1"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Wed Nov 21 11:49:03 2018"/>
+ <modified value="Tue Jan 15 18:40:09 2019"/>
+ <xMin value="-37"/>
+ <yMin value="-250"/>
+ <xMax value="582"/>
+ <yMax value="750"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="950"/>
+ <descent value="-250"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="600"/>
+ <minLeftSideBearing value="-37"/>
+ <minRightSideBearing value="-50"/>
+ <xMaxExtent value="582"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="6"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="6"/>
+ <maxPoints value="18"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="17"/>
+ <maxCompositeContours value="2"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="2"/>
+ <maxComponentDepth value="1"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="580"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="650"/>
+ <ySubscriptYSize value="600"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="75"/>
+ <ySuperscriptXSize value="650"/>
+ <ySuperscriptYSize value="600"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="350"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="300"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 01000101"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="97"/>
+ <usLastCharIndex value="775"/>
+ <sTypoAscender value="750"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="200"/>
+ <usWinAscent value="950"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="500"/>
+ <sCapHeight value="700"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="4"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="500" lsb="50"/>
+ <mtx name="a" width="600" lsb="9"/>
+ <mtx name="dotabovecomb" width="0" lsb="-37"/>
+ <mtx name="e" width="600" lsb="40"/>
+ <mtx name="edotabove" width="600" lsb="40"/>
+ <mtx name="s" width="600" lsb="25"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x65" name="e"/><!-- LATIN SMALL LETTER E -->
+ <map code="0x73" name="s"/><!-- LATIN SMALL LETTER S -->
+ <map code="0x117" name="edotabove"/><!-- LATIN SMALL LETTER E WITH DOT ABOVE -->
+ <map code="0x307" name="dotabovecomb"/><!-- COMBINING DOT ABOVE -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x65" name="e"/><!-- LATIN SMALL LETTER E -->
+ <map code="0x73" name="s"/><!-- LATIN SMALL LETTER S -->
+ <map code="0x117" name="edotabove"/><!-- LATIN SMALL LETTER E WITH DOT ABOVE -->
+ <map code="0x307" name="dotabovecomb"/><!-- COMBINING DOT ABOVE -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="50" yMin="-250" xMax="450" yMax="750">
+ <contour>
+ <pt x="50" y="-250" on="1"/>
+ <pt x="450" y="-250" on="1"/>
+ <pt x="450" y="750" on="1"/>
+ <pt x="50" y="750" on="1"/>
+ </contour>
+ <contour>
+ <pt x="100" y="-200" on="1"/>
+ <pt x="100" y="700" on="1"/>
+ <pt x="400" y="700" on="1"/>
+ <pt x="400" y="-200" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="a" xMin="9" yMin="-12" xMax="468" yMax="504">
+ <contour>
+ <pt x="468" y="-1" on="1"/>
+ <pt x="366" y="-3" on="1"/>
+ <pt x="363" y="357" on="1"/>
+ <pt x="208" y="397" on="1"/>
+ <pt x="36" y="337" on="1"/>
+ <pt x="9" y="428" on="1"/>
+ <pt x="214" y="504" on="1"/>
+ <pt x="447" y="434" on="1"/>
+ </contour>
+ <contour>
+ <pt x="378" y="263" on="1"/>
+ <pt x="382" y="207" on="1"/>
+ <pt x="88" y="172" on="1"/>
+ <pt x="86" y="126" on="1"/>
+ <pt x="161" y="74" on="1"/>
+ <pt x="383" y="134" on="1"/>
+ <pt x="389" y="71" on="1"/>
+ <pt x="168" y="-12" on="1"/>
+ <pt x="29" y="22" on="1"/>
+ <pt x="26" y="240" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="dotabovecomb" xMin="-37" yMin="501" xMax="50" yMax="597">
+ <contour>
+ <pt x="-21" y="597" on="1"/>
+ <pt x="50" y="589" on="1"/>
+ <pt x="41" y="501" on="1"/>
+ <pt x="-37" y="503" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="e" xMin="40" yMin="-18" xMax="576" yMax="513">
+ <contour>
+ <pt x="127" y="228" on="1"/>
+ <pt x="125" y="298" on="1"/>
+ <pt x="480" y="292" on="1"/>
+ <pt x="317" y="416" on="1"/>
+ <pt x="147" y="263" on="1"/>
+ <pt x="229" y="75" on="1"/>
+ <pt x="509" y="129" on="1"/>
+ <pt x="526" y="45" on="1"/>
+ <pt x="188" y="-18" on="1"/>
+ <pt x="40" y="261" on="1"/>
+ <pt x="316" y="513" on="1"/>
+ <pt x="571" y="305" on="1"/>
+ <pt x="576" y="226" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="edotabove" xMin="40" yMin="-18" xMax="576" yMax="693">
+ <component glyphName="e" x="0" y="0" flags="0x204"/>
+ <component glyphName="dotabovecomb" x="313" y="96" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="s" xMin="25" yMin="-13" xMax="582" yMax="530">
+ <contour>
+ <pt x="559" y="459" on="1"/>
+ <pt x="539" y="376" on="1"/>
+ <pt x="326" y="442" on="1"/>
+ <pt x="213" y="366" on="1"/>
+ <pt x="582" y="174" on="1"/>
+ <pt x="304" y="-13" on="1"/>
+ <pt x="25" y="83" on="1"/>
+ <pt x="53" y="174" on="1"/>
+ <pt x="282" y="76" on="1"/>
+ <pt x="427" y="155" on="1"/>
+ <pt x="38" y="343" on="1"/>
+ <pt x="324" y="530" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Layer Font
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 0.000;NONE;LayerFont-Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Layer Font Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 0.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ LayerFont-Regular
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-75"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ <psName name="dotabovecomb"/>
+ <psName name="edotabove"/>
+ </extraNames>
+ </post>
+
+ <GDEF>
+ <Version value="0x00010000"/>
+ <GlyphClassDef Format="1">
+ <ClassDef glyph="dotabovecomb" class="3"/>
+ <ClassDef glyph="e" class="1"/>
+ </GlyphClassDef>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="mark"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <MarkBasePos index="0" Format="1">
+ <MarkCoverage Format="1">
+ <Glyph value="dotabovecomb"/>
+ </MarkCoverage>
+ <BaseCoverage Format="1">
+ <Glyph value="e"/>
+ </BaseCoverage>
+ <!-- ClassCount=1 -->
+ <MarkArray>
+ <!-- MarkCount=1 -->
+ <MarkRecord index="0">
+ <Class value="0"/>
+ <MarkAnchor Format="1">
+ <XCoordinate value="-2"/>
+ <YCoordinate value="465"/>
+ </MarkAnchor>
+ </MarkRecord>
+ </MarkArray>
+ <BaseArray>
+ <!-- BaseCount=1 -->
+ <BaseRecord index="0">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="314"/>
+ <YCoordinate value="556"/>
+ </BaseAnchor>
+ </BaseRecord>
+ </BaseArray>
+ </MarkBasePos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <LigatureSubst index="0" Format="1">
+ <LigatureSet glyph="a">
+ <Ligature components="e,s,s" glyph="s"/>
+ </LigatureSet>
+ </LigatureSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx b/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx
index 23c240ef..23c240ef 100644..100755
--- a/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx
+++ b/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx
diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx
index 66c80326..84a75736 100644
--- a/Tests/varLib/data/test_results/BuildMain.ttx
+++ b/Tests/varLib/data/test_results/BuildMain.ttx
@@ -440,6 +440,9 @@
</glyf>
<name>
+ <namerecord nameID="257" platformID="0" platEncID="4" langID="0x0">
+ کنتراست
+ </namerecord>
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
Weight
</namerecord>
@@ -494,6 +497,12 @@
<namerecord nameID="273" platformID="1" platEncID="0" langID="0x0" unicode="True">
TestFamily-BlackHighContrast
</namerecord>
+ <namerecord nameID="257" platformID="1" platEncID="0" langID="0x2" unicode="True">
+ Kontrast
+ </namerecord>
+ <namerecord nameID="257" platformID="3" platEncID="1" langID="0x407">
+ Kontrast
+ </namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
Test Family
</namerecord>
@@ -2235,4 +2244,10 @@
</glyphVariations>
</gvar>
+ <ltag>
+ <version value="1"/>
+ <flags value="0"/>
+ <LanguageTag tag="fa"/>
+ </ltag>
+
</ttFont>
diff --git a/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx b/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx
index a20f0e4f..a20f0e4f 100644..100755
--- a/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx
+++ b/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx
diff --git a/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx b/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx
index 1800479b..1800479b 100644..100755
--- a/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx
+++ b/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx
diff --git a/Tests/varLib/data/test_results/SparseMasters.ttx b/Tests/varLib/data/test_results/SparseMasters.ttx
new file mode 100644
index 00000000..99a80bfb
--- /dev/null
+++ b/Tests/varLib/data/test_results/SparseMasters.ttx
@@ -0,0 +1,660 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.35">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="a"/>
+ <GlyphID id="2" name="e"/>
+ <GlyphID id="3" name="s"/>
+ <GlyphID id="4" name="dotabovecomb"/>
+ <GlyphID id="5" name="edotabove"/>
+ </GlyphOrder>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="950"/>
+ <descent value="-250"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="600"/>
+ <minLeftSideBearing value="-37"/>
+ <minRightSideBearing value="-50"/>
+ <xMaxExtent value="582"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="6"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="6"/>
+ <maxPoints value="18"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="17"/>
+ <maxCompositeContours value="2"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="2"/>
+ <maxComponentDepth value="1"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="580"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="650"/>
+ <ySubscriptYSize value="600"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="75"/>
+ <ySuperscriptXSize value="650"/>
+ <ySuperscriptYSize value="600"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="350"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="300"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 01000101"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="97"/>
+ <usLastCharIndex value="775"/>
+ <sTypoAscender value="750"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="200"/>
+ <usWinAscent value="950"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="500"/>
+ <sCapHeight value="700"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="4"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="500" lsb="50"/>
+ <mtx name="a" width="600" lsb="9"/>
+ <mtx name="dotabovecomb" width="0" lsb="-37"/>
+ <mtx name="e" width="600" lsb="40"/>
+ <mtx name="edotabove" width="600" lsb="40"/>
+ <mtx name="s" width="600" lsb="25"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x65" name="e"/><!-- LATIN SMALL LETTER E -->
+ <map code="0x73" name="s"/><!-- LATIN SMALL LETTER S -->
+ <map code="0x117" name="edotabove"/><!-- LATIN SMALL LETTER E WITH DOT ABOVE -->
+ <map code="0x307" name="dotabovecomb"/><!-- COMBINING DOT ABOVE -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x65" name="e"/><!-- LATIN SMALL LETTER E -->
+ <map code="0x73" name="s"/><!-- LATIN SMALL LETTER S -->
+ <map code="0x117" name="edotabove"/><!-- LATIN SMALL LETTER E WITH DOT ABOVE -->
+ <map code="0x307" name="dotabovecomb"/><!-- COMBINING DOT ABOVE -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="50" yMin="-250" xMax="450" yMax="750">
+ <contour>
+ <pt x="50" y="-250" on="1"/>
+ <pt x="450" y="-250" on="1"/>
+ <pt x="450" y="750" on="1"/>
+ <pt x="50" y="750" on="1"/>
+ </contour>
+ <contour>
+ <pt x="100" y="-200" on="1"/>
+ <pt x="100" y="700" on="1"/>
+ <pt x="400" y="700" on="1"/>
+ <pt x="400" y="-200" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="a" xMin="9" yMin="-12" xMax="468" yMax="504">
+ <contour>
+ <pt x="468" y="-1" on="1"/>
+ <pt x="366" y="-3" on="1"/>
+ <pt x="363" y="357" on="1"/>
+ <pt x="208" y="397" on="1"/>
+ <pt x="36" y="337" on="1"/>
+ <pt x="9" y="428" on="1"/>
+ <pt x="214" y="504" on="1"/>
+ <pt x="447" y="434" on="1"/>
+ </contour>
+ <contour>
+ <pt x="378" y="263" on="1"/>
+ <pt x="382" y="207" on="1"/>
+ <pt x="88" y="172" on="1"/>
+ <pt x="86" y="126" on="1"/>
+ <pt x="161" y="74" on="1"/>
+ <pt x="383" y="134" on="1"/>
+ <pt x="389" y="71" on="1"/>
+ <pt x="168" y="-12" on="1"/>
+ <pt x="29" y="22" on="1"/>
+ <pt x="26" y="240" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="dotabovecomb" xMin="-37" yMin="501" xMax="50" yMax="597">
+ <contour>
+ <pt x="-21" y="597" on="1"/>
+ <pt x="50" y="589" on="1"/>
+ <pt x="41" y="501" on="1"/>
+ <pt x="-37" y="503" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="e" xMin="40" yMin="-18" xMax="576" yMax="513">
+ <contour>
+ <pt x="127" y="228" on="1"/>
+ <pt x="125" y="298" on="1"/>
+ <pt x="480" y="292" on="1"/>
+ <pt x="317" y="416" on="1"/>
+ <pt x="147" y="263" on="1"/>
+ <pt x="229" y="75" on="1"/>
+ <pt x="509" y="129" on="1"/>
+ <pt x="526" y="45" on="1"/>
+ <pt x="188" y="-18" on="1"/>
+ <pt x="40" y="261" on="1"/>
+ <pt x="316" y="513" on="1"/>
+ <pt x="571" y="305" on="1"/>
+ <pt x="576" y="226" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="edotabove" xMin="40" yMin="-18" xMax="576" yMax="693">
+ <component glyphName="e" x="0" y="0" flags="0x204"/>
+ <component glyphName="dotabovecomb" x="313" y="96" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="s" xMin="25" yMin="-13" xMax="582" yMax="530">
+ <contour>
+ <pt x="559" y="459" on="1"/>
+ <pt x="539" y="376" on="1"/>
+ <pt x="326" y="442" on="1"/>
+ <pt x="213" y="366" on="1"/>
+ <pt x="582" y="174" on="1"/>
+ <pt x="304" y="-13" on="1"/>
+ <pt x="25" y="83" on="1"/>
+ <pt x="53" y="174" on="1"/>
+ <pt x="282" y="76" on="1"/>
+ <pt x="427" y="155" on="1"/>
+ <pt x="38" y="343" on="1"/>
+ <pt x="324" y="530" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Weight
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Layer Font
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 0.000;NONE;LayerFont-Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Layer Font Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 0.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ LayerFont-Regular
+ </namerecord>
+ <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
+ Weight
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-75"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ <psName name="dotabovecomb"/>
+ <psName name="edotabove"/>
+ </extraNames>
+ </post>
+
+ <GDEF>
+ <Version value="0x00010003"/>
+ <GlyphClassDef Format="1">
+ <ClassDef glyph="dotabovecomb" class="3"/>
+ <ClassDef glyph="e" class="1"/>
+ </GlyphClassDef>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=1 -->
+ <!-- RegionCount=1 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=2 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=1 -->
+ <VarRegionIndex index="0" value="0"/>
+ <Item index="0" value="[1]"/>
+ <Item index="1" value="[88]"/>
+ </VarData>
+ </VarStore>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="mark"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <MarkBasePos index="0" Format="1">
+ <MarkCoverage Format="1">
+ <Glyph value="dotabovecomb"/>
+ </MarkCoverage>
+ <BaseCoverage Format="1">
+ <Glyph value="e"/>
+ </BaseCoverage>
+ <!-- ClassCount=1 -->
+ <MarkArray>
+ <!-- MarkCount=1 -->
+ <MarkRecord index="0">
+ <Class value="0"/>
+ <MarkAnchor Format="1">
+ <XCoordinate value="-2"/>
+ <YCoordinate value="465"/>
+ </MarkAnchor>
+ </MarkRecord>
+ </MarkArray>
+ <BaseArray>
+ <!-- BaseCount=1 -->
+ <BaseRecord index="0">
+ <BaseAnchor index="0" Format="3">
+ <XCoordinate value="314"/>
+ <YCoordinate value="556"/>
+ <XDeviceTable>
+ <StartSize value="0"/>
+ <EndSize value="0"/>
+ <DeltaFormat value="32768"/>
+ </XDeviceTable>
+ <YDeviceTable>
+ <StartSize value="0"/>
+ <EndSize value="1"/>
+ <DeltaFormat value="32768"/>
+ </YDeviceTable>
+ </BaseAnchor>
+ </BaseRecord>
+ </BaseArray>
+ </MarkBasePos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <LigatureSubst index="0" Format="1">
+ <LigatureSet glyph="a">
+ <Ligature components="e,s,s" glyph="s"/>
+ </LigatureSet>
+ </LigatureSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+ <HVAR>
+ <Version value="0x00010000"/>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=1 -->
+ <!-- RegionCount=3 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.36365"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.36365"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="2">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=1 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=0 -->
+ <Item index="0" value="[]"/>
+ </VarData>
+ </VarStore>
+ <AdvWidthMap>
+ <Map glyph=".notdef" outer="0" inner="0"/>
+ <Map glyph="a" outer="0" inner="0"/>
+ <Map glyph="dotabovecomb" outer="0" inner="0"/>
+ <Map glyph="e" outer="0" inner="0"/>
+ <Map glyph="edotabove" outer="0" inner="0"/>
+ <Map glyph="s" outer="0" inner="0"/>
+ </AdvWidthMap>
+ </HVAR>
+
+ <MVAR>
+ <Version value="0x00010000"/>
+ <Reserved value="0"/>
+ <ValueRecordSize value="8"/>
+ <!-- ValueRecordCount=1 -->
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=1 -->
+ <!-- RegionCount=1 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=1 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=1 -->
+ <VarRegionIndex index="0" value="0"/>
+ <Item index="0" value="[-25]"/>
+ </VarData>
+ </VarStore>
+ <ValueRecord index="0">
+ <ValueTag value="undo"/>
+ <VarIdx value="0"/>
+ </ValueRecord>
+ </MVAR>
+
+ <STAT>
+ <Version value="0x00010001"/>
+ <DesignAxisRecordSize value="8"/>
+ <!-- DesignAxisCount=1 -->
+ <DesignAxisRecord>
+ <Axis index="0">
+ <AxisTag value="wght"/>
+ <AxisNameID value="256"/> <!-- Weight -->
+ <AxisOrdering value="0"/>
+ </Axis>
+ </DesignAxisRecord>
+ <!-- AxisValueCount=0 -->
+ <ElidedFallbackNameID value="2"/> <!-- Regular -->
+ </STAT>
+
+ <fvar>
+
+ <!-- Weight -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>350.0</MinValue>
+ <DefaultValue>350.0</DefaultValue>
+ <MaxValue>625.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+ </fvar>
+
+ <gvar>
+ <version value="1"/>
+ <reserved value="0"/>
+ <glyphVariations glyph="a">
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="-59" y="2"/>
+ <delta pt="2" x="-59" y="-54"/>
+ <delta pt="3" x="0" y="-56"/>
+ <delta pt="4" x="0" y="-56"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ <delta pt="8" x="0" y="0"/>
+ <delta pt="9" x="-1" y="-23"/>
+ <delta pt="10" x="77" y="7"/>
+ <delta pt="11" x="77" y="7"/>
+ <delta pt="12" x="40" y="28"/>
+ <delta pt="13" x="0" y="15"/>
+ <delta pt="14" x="0" y="0"/>
+ <delta pt="15" x="0" y="0"/>
+ <delta pt="16" x="0" y="0"/>
+ <delta pt="17" x="0" y="0"/>
+ <delta pt="18" x="0" y="0"/>
+ <delta pt="19" x="0" y="0"/>
+ <delta pt="20" x="0" y="0"/>
+ <delta pt="21" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="e">
+ <tuple>
+ <coord axis="wght" max="1.0" min="0.0" value="0.36365"/>
+ <delta pt="0" x="-1" y="-25"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="-84" y="5"/>
+ <delta pt="3" x="1" y="-29"/>
+ <delta pt="4" x="33" y="1"/>
+ <delta pt="5" x="35" y="41"/>
+ <delta pt="6" x="-2" y="28"/>
+ <delta pt="7" x="0" y="0"/>
+ <delta pt="8" x="0" y="0"/>
+ <delta pt="9" x="0" y="0"/>
+ <delta pt="10" x="0" y="0"/>
+ <delta pt="11" x="0" y="0"/>
+ <delta pt="12" x="0" y="-27"/>
+ <delta pt="13" x="0" y="0"/>
+ <delta pt="14" x="0" y="0"/>
+ <delta pt="15" x="0" y="0"/>
+ <delta pt="16" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" max="1.0" min="0.36365" value="1.0"/>
+ <delta pt="0" x="70" y="1"/>
+ <delta pt="1" x="70" y="1"/>
+ <delta pt="2" x="-76" y="1"/>
+ <delta pt="3" x="-16" y="-56"/>
+ <delta pt="4" x="70" y="1"/>
+ <delta pt="5" x="15" y="55"/>
+ <delta pt="6" x="15" y="55"/>
+ <delta pt="7" x="2" y="-45"/>
+ <delta pt="8" x="0" y="0"/>
+ <delta pt="9" x="-31" y="1"/>
+ <delta pt="10" x="-2" y="35"/>
+ <delta pt="11" x="25" y="-1"/>
+ <delta pt="12" x="25" y="-1"/>
+ <delta pt="13" x="0" y="0"/>
+ <delta pt="14" x="0" y="0"/>
+ <delta pt="15" x="0" y="35"/>
+ <delta pt="16" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="s">
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="-2" y="-40"/>
+ <delta pt="2" x="-2" y="-40"/>
+ <delta pt="3" x="55" y="-9"/>
+ <delta pt="4" x="26" y="-33"/>
+ <delta pt="5" x="-20" y="-45"/>
+ <delta pt="6" x="-18" y="-4"/>
+ <delta pt="7" x="-27" y="52"/>
+ <delta pt="8" x="-61" y="43"/>
+ <delta pt="9" x="-80" y="-6"/>
+ <delta pt="10" x="-22" y="55"/>
+ <delta pt="11" x="0" y="0"/>
+ <delta pt="12" x="0" y="0"/>
+ <delta pt="13" x="0" y="0"/>
+ <delta pt="14" x="0" y="0"/>
+ <delta pt="15" x="0" y="45"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="dotabovecomb">
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="-8" y="28"/>
+ <delta pt="1" x="13" y="16"/>
+ <delta pt="2" x="17" y="-13"/>
+ <delta pt="3" x="-27" y="-20"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="28"/>
+ <delta pt="7" x="0" y="18"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="edotabove">
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="1" x="-6" y="91"/>
+ <delta pt="4" x="0" y="119"/>
+ </tuple>
+ </glyphVariations>
+ </gvar>
+
+</ttFont>
diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py
index 6638ea36..831d8b83 100644
--- a/Tests/varLib/varLib_test.py
+++ b/Tests/varLib/varLib_test.py
@@ -290,7 +290,7 @@ class BuildTest(unittest.TestCase):
expected_ttx_path = self.get_test_output('BuildMain.ttx')
self.expect_ttx(varfont, expected_ttx_path, tables)
- def test_varlib_build_from_ds_object(self):
+ def test_varlib_build_from_ds_object_in_memory_ttfonts(self):
ds_path = self.get_test_input("Build.designspace")
ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
expected_ttx_path = self.get_test_output("BuildMain.ttx")
@@ -314,6 +314,53 @@ class BuildTest(unittest.TestCase):
tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
self.expect_ttx(varfont, expected_ttx_path, tables)
+ def test_varlib_build_from_ttf_paths(self):
+ ds_path = self.get_test_input("Build.designspace")
+ ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
+ expected_ttx_path = self.get_test_output("BuildMain.ttx")
+
+ self.temp_dir()
+ for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'):
+ self.compile_font(path, ".ttf", self.tempdir)
+
+ ds = DesignSpaceDocument.fromfile(ds_path)
+ for source in ds.sources:
+ source.path = os.path.join(
+ self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf")
+ )
+ ds.updatePaths()
+
+ varfont, _, _ = build(ds)
+ varfont = reload_font(varfont)
+ tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
+ self.expect_ttx(varfont, expected_ttx_path, tables)
+
+ def test_varlib_build_from_ttx_paths(self):
+ ds_path = self.get_test_input("Build.designspace")
+ ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
+ expected_ttx_path = self.get_test_output("BuildMain.ttx")
+
+ ds = DesignSpaceDocument.fromfile(ds_path)
+ for source in ds.sources:
+ source.path = os.path.join(
+ ttx_dir, os.path.basename(source.filename).replace(".ufo", ".ttx")
+ )
+ ds.updatePaths()
+
+ varfont, _, _ = build(ds)
+ varfont = reload_font(varfont)
+ tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
+ self.expect_ttx(varfont, expected_ttx_path, tables)
+
+ def test_varlib_build_sparse_masters(self):
+ ds_path = self.get_test_input("SparseMasters.designspace")
+ expected_ttx_path = self.get_test_output("SparseMasters.ttx")
+
+ varfont, _, _ = build(ds_path)
+ varfont = reload_font(varfont)
+ tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
+ self.expect_ttx(varfont, expected_ttx_path, tables)
+
def test_load_masters_layerName_without_required_font():
ds = DesignSpaceDocument()
diff --git a/Tests/voltLib/parser_test.py b/Tests/voltLib/parser_test.py
index 6baf9003..51a65fc8 100644
--- a/Tests/voltLib/parser_test.py
+++ b/Tests/voltLib/parser_test.py
@@ -1,5 +1,6 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
+from fontTools.voltLib import ast
from fontTools.voltLib.error import VoltLibError
from fontTools.voltLib.parser import Parser
from io import open
@@ -76,6 +77,22 @@ class ParserTest(unittest.TestCase):
def_glyph.type, def_glyph.components),
("f_f", 320, None, "LIGATURE", 2))
+ def test_def_glyph_mark(self):
+ [def_glyph] = self.parse(
+ 'DEF_GLYPH "brevecomb" ID 320 TYPE MARK END_GLYPH'
+ ).statements
+ self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
+ def_glyph.type, def_glyph.components),
+ ("brevecomb", 320, None, "MARK", None))
+
+ def test_def_glyph_component(self):
+ [def_glyph] = self.parse(
+ 'DEF_GLYPH "f.f_f" ID 320 TYPE COMPONENT END_GLYPH'
+ ).statements
+ self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
+ def_glyph.type, def_glyph.components),
+ ("f.f_f", 320, None, "COMPONENT", None))
+
def test_def_glyph_no_type(self):
[def_glyph] = self.parse(
'DEF_GLYPH "glyph20" ID 20 END_GLYPH'
@@ -106,14 +123,14 @@ class ParserTest(unittest.TestCase):
'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
'END_GROUP\n'
).statements
- self.assertEqual((def_group.name, def_group.enum),
+ self.assertEqual((def_group.name, def_group.enum.glyphSet()),
("aaccented",
("aacute", "abreve", "acircumflex", "adieresis",
"ae", "agrave", "amacron", "aogonek", "aring",
"atilde")))
def test_def_group_groups(self):
- [group1, group2, test_group] = self.parse(
+ parser = self.parser(
'DEF_GROUP "Group1"\n'
'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
'END_GROUP\n'
@@ -123,14 +140,16 @@ class ParserTest(unittest.TestCase):
'DEF_GROUP "TestGroup"\n'
'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
'END_GROUP\n'
- ).statements
+ )
+ [group1, group2, test_group] = parser.parse().statements
self.assertEqual(
(test_group.name, test_group.enum),
("TestGroup",
- (("Group1",), ("Group2",))))
+ ast.Enum([ast.GroupName("Group1", parser),
+ ast.GroupName("Group2", parser)])))
def test_def_group_groups_not_yet_defined(self):
- [group1, test_group1, test_group2, test_group3, group2] = self.parse(
+ parser = self.parser(
'DEF_GROUP "Group1"\n'
'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
'END_GROUP\n'
@@ -146,19 +165,23 @@ class ParserTest(unittest.TestCase):
'DEF_GROUP "Group2"\n'
'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
'END_GROUP\n'
- ).statements
+ )
+ [group1, test_group1, test_group2, test_group3, group2] = \
+ parser.parse().statements
self.assertEqual(
(test_group1.name, test_group1.enum),
("TestGroup1",
- (("Group1", ), ("Group2", ))))
+ ast.Enum([ast.GroupName("Group1", parser),
+ ast.GroupName("Group2", parser)])))
self.assertEqual(
(test_group2.name, test_group2.enum),
("TestGroup2",
- (("Group2", ), )))
+ ast.Enum([ast.GroupName("Group2", parser)])))
self.assertEqual(
(test_group3.name, test_group3.enum),
("TestGroup3",
- (("Group2", ), ("Group1", ))))
+ ast.Enum([ast.GroupName("Group2", parser),
+ ast.GroupName("Group1", parser)])))
# def test_def_group_groups_undefined(self):
# with self.assertRaisesRegex(
@@ -184,20 +207,30 @@ class ParserTest(unittest.TestCase):
'ENUM GLYPH "a" GROUP "aaccented" END_ENUM\n'
'END_GROUP'
).statements
- self.assertEqual((def_group2.name, def_group2.enum),
- ("KERN_lc_a_2ND",
- ("a", ("aaccented", ))))
+ items = def_group2.enum.enum
+ self.assertEqual((def_group2.name, items[0].glyphSet(), items[1].group),
+ ("KERN_lc_a_2ND", ("a",), "aaccented"))
def test_def_group_range(self):
- [def_group] = self.parse(
+ def_group = self.parse(
+ 'DEF_GLYPH "a" ID 163 UNICODE 97 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "agrave" ID 194 UNICODE 224 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "aacute" ID 195 UNICODE 225 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "acircumflex" ID 196 UNICODE 226 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "atilde" ID 197 UNICODE 227 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "c" ID 165 UNICODE 99 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "ccaron" ID 209 UNICODE 269 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "ccedilla" ID 210 UNICODE 231 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "cdotaccent" ID 210 UNICODE 267 TYPE BASE END_GLYPH\n'
'DEF_GROUP "KERN_lc_a_2ND"\n'
'ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" '
'END_ENUM\n'
'END_GROUP'
- ).statements
- self.assertEqual((def_group.name, def_group.enum),
+ ).statements[-1]
+ self.assertEqual((def_group.name, def_group.enum.glyphSet()),
("KERN_lc_a_2ND",
- (("a", "atilde"), "b", ("c", "cdotaccent"))))
+ ("a", "agrave", "aacute", "acircumflex", "atilde",
+ "b", "c", "ccaron", "ccedilla", "cdotaccent")))
def test_group_duplicate(self):
self.assertRaisesRegex(
@@ -433,10 +466,10 @@ class ParserTest(unittest.TestCase):
def test_lookup_name_starts_with_letter(self):
with self.assertRaisesRegex(
VoltLibError,
- 'Lookup name "\\\lookupname" must start with a letter'
+ r'Lookup name "\\lookupname" must start with a letter'
):
[lookup] = self.parse(
- 'DEF_LOOKUP "\lookupname"\n'
+ 'DEF_LOOKUP "\\lookupname"\n'
'AS_SUBSTITUTION\n'
'SUB GLYPH "a"\n'
'WITH GLYPH "a.alt"\n'
@@ -524,10 +557,11 @@ class ParserTest(unittest.TestCase):
'END_SUBSTITUTION'
).statements
self.assertEqual((lookup.name, list(lookup.sub.mapping.items())),
- ("smcp", [(("a",), ("a.sc",)), (("b",), ("b.sc",))]))
+ ("smcp", [(self.enum(["a"]), self.enum(["a.sc"])),
+ (self.enum(["b"]), self.enum(["b.sc"]))]))
def test_substitution_single_in_context(self):
- [group, lookup] = self.parse(
+ parser = self.parser(
'DEF_GROUP "Denominators" ENUM GLYPH "one.dnom" GLYPH "two.dnom" '
'END_ENUM END_GROUP\n'
'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL '
@@ -543,17 +577,22 @@ class ParserTest(unittest.TestCase):
'WITH GLYPH "two.dnom"\n'
'END_SUB\n'
'END_SUBSTITUTION'
- ).statements
+ )
+ [group, lookup] = parser.parse().statements
context = lookup.context[0]
self.assertEqual(
(lookup.name, list(lookup.sub.mapping.items()),
context.ex_or_in, context.left, context.right),
- ("fracdnom", [(("one",), ("one.dnom",)), (("two",), ("two.dnom",))],
- "IN_CONTEXT", [((("Denominators",), "fraction"),)], [])
+ ("fracdnom",
+ [(self.enum(["one"]), self.enum(["one.dnom"])),
+ (self.enum(["two"]), self.enum(["two.dnom"]))],
+ "IN_CONTEXT", [ast.Enum([
+ ast.GroupName("Denominators", parser=parser),
+ ast.GlyphName("fraction")])], [])
)
def test_substitution_single_in_contexts(self):
- [group, lookup] = self.parse(
+ parser = self.parser(
'DEF_GROUP "Hebrew" ENUM GLYPH "uni05D0" GLYPH "uni05D1" '
'END_ENUM END_GROUP\n'
'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL '
@@ -571,7 +610,8 @@ class ParserTest(unittest.TestCase):
'WITH GLYPH "dollar.Hebr"\n'
'END_SUB\n'
'END_SUBSTITUTION'
- ).statements
+ )
+ [group, lookup] = parser.parse().statements
context1 = lookup.context[0]
context2 = lookup.context[1]
self.assertEqual(
@@ -579,8 +619,10 @@ class ParserTest(unittest.TestCase):
context1.right, context2.ex_or_in,
context2.left, context2.right),
("HebrewCurrency", "IN_CONTEXT", [],
- [(("Hebrew",),), ("one.Hebr",)], "IN_CONTEXT",
- [(("Hebrew",),), ("one.Hebr",)], []))
+ [ast.Enum([ast.GroupName("Hebrew", parser)]),
+ self.enum(["one.Hebr"])], "IN_CONTEXT",
+ [ast.Enum([ast.GroupName("Hebrew", parser)]),
+ self.enum(["one.Hebr"])], []))
def test_substitution_skip_base(self):
[group, lookup] = self.parse(
@@ -596,9 +638,8 @@ class ParserTest(unittest.TestCase):
'END_SUB\n'
'END_SUBSTITUTION'
).statements
- process_base = lookup.process_base
self.assertEqual(
- (lookup.name, process_base),
+ (lookup.name, lookup.process_base),
("SomeSub", False))
def test_substitution_process_base(self):
@@ -615,9 +656,8 @@ class ParserTest(unittest.TestCase):
'END_SUB\n'
'END_SUBSTITUTION'
).statements
- process_base = lookup.process_base
self.assertEqual(
- (lookup.name, process_base),
+ (lookup.name, lookup.process_base),
("SomeSub", True))
def test_substitution_skip_marks(self):
@@ -634,12 +674,11 @@ class ParserTest(unittest.TestCase):
'END_SUB\n'
'END_SUBSTITUTION'
).statements
- process_marks = lookup.process_marks
self.assertEqual(
- (lookup.name, process_marks),
+ (lookup.name, lookup.process_marks),
("SomeSub", False))
- def test_substitution_process_marks(self):
+ def test_substitution_mark_attachment(self):
[group, lookup] = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" '
'END_ENUM END_GROUP\n'
@@ -652,9 +691,25 @@ class ParserTest(unittest.TestCase):
'END_SUB\n'
'END_SUBSTITUTION'
).statements
- process_marks = lookup.process_marks
self.assertEqual(
- (lookup.name, process_marks),
+ (lookup.name, lookup.process_marks),
+ ("SomeSub", "SomeMarks"))
+
+ def test_substitution_mark_glyph_set(self):
+ [group, lookup] = self.parse(
+ 'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" '
+ 'END_ENUM END_GROUP\n'
+ 'DEF_LOOKUP "SomeSub" PROCESS_BASE '
+ 'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" \n'
+ 'DIRECTION RTL\n'
+ 'AS_SUBSTITUTION\n'
+ 'SUB GLYPH "A"\n'
+ 'WITH GLYPH "A.c2sc"\n'
+ 'END_SUB\n'
+ 'END_SUBSTITUTION'
+ ).statements
+ self.assertEqual(
+ (lookup.name, lookup.mark_glyph_set),
("SomeSub", "SomeMarks"))
def test_substitution_process_all_marks(self):
@@ -670,9 +725,8 @@ class ParserTest(unittest.TestCase):
'END_SUB\n'
'END_SUBSTITUTION'
).statements
- process_marks = lookup.process_marks
self.assertEqual(
- (lookup.name, process_marks),
+ (lookup.name, lookup.process_marks),
("SomeSub", True))
def test_substitution_no_reversal(self):
@@ -695,7 +749,13 @@ class ParserTest(unittest.TestCase):
)
def test_substitution_reversal(self):
- [lookup] = self.parse(
+ lookup = self.parse(
+ 'DEF_GROUP "DFLT_Num_standardFigures"\n'
+ 'ENUM GLYPH "zero" GLYPH "one" GLYPH "two" END_ENUM\n'
+ 'END_GROUP\n'
+ 'DEF_GROUP "DFLT_Num_numerators"\n'
+ 'ENUM GLYPH "zero.numr" GLYPH "one.numr" GLYPH "two.numr" END_ENUM\n'
+ 'END_GROUP\n'
'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR REVERSAL\n'
'IN_CONTEXT\n'
@@ -706,7 +766,7 @@ class ParserTest(unittest.TestCase):
'WITH GROUP "DFLT_Num_numerators"\n'
'END_SUB\n'
'END_SUBSTITUTION'
- ).statements
+ ).statements[-1]
self.assertEqual(
(lookup.name, lookup.reversal),
("RevLookup", True)
@@ -729,8 +789,8 @@ class ParserTest(unittest.TestCase):
).statements
self.assertEqual((lookup.name, list(lookup.sub.mapping.items())),
("ccmp",
- [(("aacute",), ("a", "acutecomb")),
- (("agrave",), ("a", "gravecomb"))]
+ [(self.enum(["aacute"]), self.enum(["a", "acutecomb"])),
+ (self.enum(["agrave"]), self.enum(["a", "gravecomb"]))]
))
def test_substitution_multiple_to_single(self):
@@ -750,11 +810,31 @@ class ParserTest(unittest.TestCase):
).statements
self.assertEqual((lookup.name, list(lookup.sub.mapping.items())),
("liga",
- [(("f", "i"), ("f_i",)),
- (("f", "t"), ("f_t",))]))
+ [(self.enum(["f", "i"]), self.enum(["f_i"])),
+ (self.enum(["f", "t"]), self.enum(["f_t"]))]))
def test_substitution_reverse_chaining_single(self):
- [lookup] = self.parse(
+ parser = self.parser(
+ 'DEF_GLYPH "zero" ID 1 UNICODE 48 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "one" ID 2 UNICODE 49 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "two" ID 3 UNICODE 50 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "three" ID 4 UNICODE 51 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "four" ID 5 UNICODE 52 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "five" ID 6 UNICODE 53 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "six" ID 7 UNICODE 54 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "seven" ID 8 UNICODE 55 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "eight" ID 9 UNICODE 56 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "nine" ID 10 UNICODE 57 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "zero.numr" ID 11 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "one.numr" ID 12 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "two.numr" ID 13 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "three.numr" ID 14 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "four.numr" ID 15 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "five.numr" ID 16 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "six.numr" ID 17 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "seven.numr" ID 18 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "eight.numr" ID 19 TYPE BASE END_GLYPH\n'
+ 'DEF_GLYPH "nine.numr" ID 20 TYPE BASE END_GLYPH\n'
'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR REVERSAL\n'
'IN_CONTEXT\n'
@@ -768,12 +848,16 @@ class ParserTest(unittest.TestCase):
'WITH RANGE "zero.numr" TO "nine.numr"\n'
'END_SUB\n'
'END_SUBSTITUTION'
- ).statements
+ )
+ lookup = parser.parse().statements[-1]
self.assertEqual(
(lookup.name, lookup.context[0].right,
list(lookup.sub.mapping.items())),
- ("numr", [(("fraction", ("zero.numr", "nine.numr")),)],
- [((("zero", "nine"),), (("zero.numr", "nine.numr"),))]))
+ ("numr",
+ [(ast.Enum([ast.GlyphName("fraction"),
+ ast.Range("zero.numr", "nine.numr", parser)]))],
+ [(ast.Enum([ast.Range("zero", "nine", parser)]),
+ ast.Enum([ast.Range("zero.numr", "nine.numr", parser)]))]))
# GPOS
# ATTACH_CURSIVE
@@ -817,8 +901,9 @@ class ParserTest(unittest.TestCase):
).statements
self.assertEqual(
(lookup.name, lookup.pos.coverage, lookup.pos.coverage_to),
- ("anchor_top", ("a", "e"), [(("acutecomb",), "top"),
- (("gravecomb",), "top")])
+ ("anchor_top", self.enum(["a", "e"]),
+ [(self.enum(["acutecomb"]), "top"),
+ (self.enum(["gravecomb"]), "top")])
)
self.assertEqual(
(anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component,
@@ -858,7 +943,7 @@ class ParserTest(unittest.TestCase):
(lookup.name,
lookup.pos.coverages_exit, lookup.pos.coverages_enter),
("SomeLookup",
- [("a", "b")], [("c",)])
+ [self.enum(["a", "b"])], [self.enum(["c"])])
)
def test_position_adjust_pair(self):
@@ -879,7 +964,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(
(lookup.name, lookup.pos.coverages_1, lookup.pos.coverages_2,
lookup.pos.adjust_pair),
- ("kern1", [("A",)], [("V",)],
+ ("kern1", [self.enum(["A"])], [self.enum(["V"])],
{(1, 2): ((-30, None, None, {}, {}, {}),
(None, None, None, {}, {}, {})),
(2, 1): ((-30, None, None, {}, {}, {}),
@@ -904,8 +989,8 @@ class ParserTest(unittest.TestCase):
self.assertEqual(
(lookup.name, lookup.pos.adjust_single),
("TestLookup",
- [(("glyph1",), (0, 123, None, {}, {}, {})),
- (("glyph2",), (0, 456, None, {}, {}, {}))])
+ [(self.enum(["glyph1"]), (0, 123, None, {}, {}, {})),
+ (self.enum(["glyph2"]), (0, 456, None, {}, {}, {}))])
)
def test_def_anchor(self):
@@ -936,6 +1021,22 @@ class ParserTest(unittest.TestCase):
False, (None, 250, 0, {}, {}, {}))
)
+ def test_def_anchor_multi_component(self):
+ [anchor1, anchor2] = self.parse(
+ 'DEF_ANCHOR "top" ON 120 GLYPH a '
+ 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n'
+ 'DEF_ANCHOR "top" ON 120 GLYPH a '
+ 'COMPONENT 2 AT POS DX 250 DY 450 END_POS END_ANCHOR\n'
+ ).statements
+ self.assertEqual(
+ (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component),
+ ("top", 120, "a", 1)
+ )
+ self.assertEqual(
+ (anchor2.name, anchor2.gid, anchor2.glyph_name, anchor2.component),
+ ("top", 120, "a", 2)
+ )
+
def test_def_anchor_duplicate(self):
self.assertRaisesRegex(
VoltLibError,
@@ -1012,6 +1113,14 @@ class ParserTest(unittest.TestCase):
("CMAP_FORMAT", (3, 1, 4)))
)
+ def test_stop_at_end(self):
+ [def_glyph] = self.parse(
+ 'DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH END\0\0\0\0'
+ ).statements
+ self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
+ def_glyph.type, def_glyph.components),
+ (".notdef", 0, None, "BASE", None))
+
def setUp(self):
self.tempdir = None
self.num_tempfiles = 0
@@ -1020,14 +1129,20 @@ class ParserTest(unittest.TestCase):
if self.tempdir:
shutil.rmtree(self.tempdir)
- def parse(self, text):
+ def parser(self, text):
if not self.tempdir:
self.tempdir = tempfile.mkdtemp()
self.num_tempfiles += 1
path = os.path.join(self.tempdir, "tmp%d.vtp" % self.num_tempfiles)
with open(path, "w") as outfile:
outfile.write(text)
- return Parser(path).parse()
+ return Parser(path)
+
+ def parse(self, text):
+ return self.parser(text).parse()
+
+ def enum(self, glyphs):
+ return ast.Enum([ast.GlyphName(g) for g in glyphs])
if __name__ == "__main__":
import sys
diff --git a/fonttools b/fonttools
index 92b390e7..92b390e7 100644..100755
--- a/fonttools
+++ b/fonttools
diff --git a/requirements.txt b/requirements.txt
index c9ac4f39..b910fea7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,4 +6,4 @@ unicodedata2==11.0.0; python_version < '3.7' and platform_python_implementation
scipy==1.2.0; platform_python_implementation != "PyPy"
munkres==1.0.12; platform_python_implementation == "PyPy"
zopfli==0.1.6
-fs==2.1.3
+fs==2.2.1
diff --git a/run-tests.sh b/run-tests.sh
index f10c1b01..f10c1b01 100644..100755
--- a/run-tests.sh
+++ b/run-tests.sh
diff --git a/setup.cfg b/setup.cfg
index 6ee25421..97d51aef 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 3.35.0
+current_version = 3.37.0
commit = True
tag = False
tag_name = {new_version}
diff --git a/setup.py b/setup.py
index 25093cd4..f22309da 100644..100755
--- a/setup.py
+++ b/setup.py
@@ -31,7 +31,7 @@ bumpversion = ['bump2version'] if needs_bumpversion else []
extras_require = {
# for fontTools.ufoLib: to read/write UFO fonts
"ufo": [
- "fs >= 2.1.1, < 3",
+ "fs >= 2.2.0, < 3",
"enum34 >= 1.1.6; python_version < '3.4'",
],
# for fontTools.misc.etree and fontTools.misc.plistlib: use lxml to
@@ -352,7 +352,7 @@ def find_data_files(manpath="share/man"):
setup(
name="fonttools",
- version="3.35.0",
+ version="3.37.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",