aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2019-01-10 11:08:18 -0800
committerandroid-build-merger <android-build-merger@google.com>2019-01-10 11:08:18 -0800
commit0ed57b6ef49adea72c7f51bb57f812334a605420 (patch)
treefeec98f7887269a73346929e7d1a295df78936c8
parent7b66ac5cf864ac90df7593ed3b560a8e16cedcc9 (diff)
parent7ce473d6ec491a3e7a87fa646f025e85ac31ab2c (diff)
downloadfonttools-0ed57b6ef49adea72c7f51bb57f812334a605420.tar.gz
Merge "Upgrade fonttools from 3.31.0 to 3.35.0"
am: 7ce473d6ec Change-Id: I0d9a7d9381f534fcefbd0f775fc600d0c36af36b
-rw-r--r--.appveyor.yml11
-rw-r--r--.travis.yml10
-rw-r--r--[-rwxr-xr-x].travis/after_success.sh0
-rw-r--r--[-rwxr-xr-x].travis/before_install.sh0
-rw-r--r--[-rwxr-xr-x].travis/install.sh0
-rw-r--r--[-rwxr-xr-x].travis/run.sh8
-rw-r--r--Doc/source/designspaceLib/readme.rst19
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/cffLib/__init__.py16
-rw-r--r--Lib/fontTools/cffLib/specializer.py6
-rw-r--r--Lib/fontTools/designspaceLib/__init__.py26
-rw-r--r--Lib/fontTools/fontBuilder.py765
-rw-r--r--Lib/fontTools/merge.py6
-rw-r--r--Lib/fontTools/misc/dictTools.py68
-rw-r--r--Lib/fontTools/misc/intTools.py18
-rw-r--r--Lib/fontTools/misc/macRes.py2
-rw-r--r--Lib/fontTools/misc/plistlib.py9
-rw-r--r--Lib/fontTools/misc/psCharStrings.py70
-rw-r--r--Lib/fontTools/mtiLib/__init__.py3
-rw-r--r--Lib/fontTools/pens/pointPen.py22
-rw-r--r--Lib/fontTools/pens/t2CharStringPen.py28
-rw-r--r--Lib/fontTools/subset/__init__.py554
-rw-r--r--Lib/fontTools/subset/cff.py579
-rw-r--r--Lib/fontTools/t1Lib/__init__.py59
-rw-r--r--Lib/fontTools/ttLib/sfnt.py24
-rw-r--r--Lib/fontTools/ttLib/tables/F__e_a_t.py4
-rw-r--r--Lib/fontTools/ttLib/tables/G__l_a_t.py17
-rw-r--r--Lib/fontTools/ttLib/tables/S__i_l_f.py53
-rw-r--r--Lib/fontTools/ttLib/tables/S__i_l_l.py4
-rw-r--r--Lib/fontTools/ttLib/tables/_k_e_r_n.py19
-rw-r--r--Lib/fontTools/ttLib/tables/_n_a_m_e.py25
-rw-r--r--Lib/fontTools/ttLib/tables/grUtils.py10
-rw-r--r--[-rwxr-xr-x]Lib/fontTools/ttLib/tables/otData.py0
-rw-r--r--Lib/fontTools/ttLib/tables/ttProgram.py7
-rw-r--r--Lib/fontTools/ttx.py6
-rw-r--r--[-rwxr-xr-x]Lib/fontTools/ufoLib/__init__.py253
-rw-r--r--[-rwxr-xr-x]Lib/fontTools/ufoLib/glifLib.py8
-rw-r--r--Lib/fontTools/unicode.py7
-rw-r--r--Lib/fontTools/varLib/__init__.py271
-rw-r--r--Lib/fontTools/varLib/builder.py21
-rw-r--r--Lib/fontTools/varLib/cff.py502
-rw-r--r--Lib/fontTools/varLib/featureVars.py337
-rw-r--r--Lib/fontTools/varLib/interpolate_layout.py24
-rw-r--r--Lib/fontTools/varLib/merger.py303
-rw-r--r--Lib/fontTools/varLib/models.py102
-rw-r--r--Lib/fontTools/varLib/mutator.py280
-rw-r--r--Lib/fontTools/varLib/varStore.py143
-rw-r--r--Lib/fonttools.egg-info/PKG-INFO107
-rw-r--r--Lib/fonttools.egg-info/SOURCES.txt65
-rw-r--r--Lib/fonttools.egg-info/requires.txt10
-rw-r--r--METADATA10
-rw-r--r--[-rwxr-xr-x]MetaTools/buildTableList.py18
-rw-r--r--[-rwxr-xr-x]MetaTools/buildUCD.py0
-rw-r--r--[-rwxr-xr-x]MetaTools/roundTrip.py33
-rw-r--r--NEWS.rst76
-rw-r--r--PKG-INFO107
-rw-r--r--README.rst17
-rw-r--r--[-rwxr-xr-x]Snippets/cmap-format.py0
-rw-r--r--[-rwxr-xr-x]Snippets/interpolate.py0
-rw-r--r--[-rwxr-xr-x]Snippets/layout-features.py0
-rw-r--r--[-rwxr-xr-x]Snippets/otf2ttf.py0
-rw-r--r--[-rwxr-xr-x]Snippets/rename-fonts.py0
-rw-r--r--[-rwxr-xr-x]Snippets/subset-fpgm.py0
-rw-r--r--[-rwxr-xr-x]Snippets/svg2glif.py0
-rw-r--r--[-rwxr-xr-x]Snippets/woff2_compress.py0
-rw-r--r--[-rwxr-xr-x]Snippets/woff2_decompress.py0
-rw-r--r--Tests/designspaceLib/designspace_test.py92
-rw-r--r--Tests/fontBuilder/data/test.otf.ttx253
-rw-r--r--Tests/fontBuilder/data/test.ttf.ttx270
-rw-r--r--Tests/fontBuilder/data/test_var.otf.ttx291
-rw-r--r--Tests/fontBuilder/data/test_var.ttf.ttx376
-rw-r--r--Tests/fontBuilder/fontBuilder_test.py246
-rw-r--r--Tests/misc/plistlib_test.py23
-rw-r--r--Tests/misc/psCharStrings_test.py43
-rw-r--r--Tests/pens/pointPen_test.py412
-rw-r--r--Tests/t1Lib/t1Lib_test.py6
-rw-r--r--Tests/ttLib/tables/_k_e_r_n_test.py51
-rw-r--r--Tests/ttLib/tables/_n_a_m_e_test.py4
-rw-r--r--[-rwxr-xr-x]Tests/ufoLib/testSupport.py0
-rw-r--r--Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace22
-rw-r--r--Tests/varLib/data/FeatureVars.designspace2
-rw-r--r--Tests/varLib/data/TestCFF2.designspace93
-rw-r--r--Tests/varLib/data/TestCFF2VF.otfbin0 -> 3636 bytes
-rw-r--r--Tests/varLib/data/master_cff2/TestCFF2_Black.otfbin0 -> 2272 bytes
-rw-r--r--Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otfbin0 -> 2300 bytes
-rw-r--r--Tests/varLib/data/master_cff2/TestCFF2_Regular.otfbin0 -> 2280 bytes
-rw-r--r--Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx555
-rw-r--r--Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx662
-rw-r--r--Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx656
-rw-r--r--[-rwxr-xr-x]Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx0
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/features.fea23
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist93
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif41
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif27
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist14
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif15
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif37
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif47
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif51
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif23
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist20
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif48
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif44
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif50
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif22
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist34
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist14
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist86
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist10
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea23
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist93
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif31
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif28
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist16
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif15
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif14
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif37
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif47
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif51
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif23
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist20
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif48
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif44
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif50
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif22
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist34
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist468
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist14
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist80
-rw-r--r--Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist10
-rw-r--r--Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx229
-rw-r--r--Tests/varLib/data/test_results/BuildMain.ttx2
-rw-r--r--Tests/varLib/data/test_results/BuildTestCFF2.ttx265
-rw-r--r--Tests/varLib/data/test_results/FeatureVars.ttx34
-rw-r--r--Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx107
-rw-r--r--Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx297
-rw-r--r--[-rwxr-xr-x]Tests/varLib/data/test_results/Mutator_IUP-instance.ttx0
-rw-r--r--Tests/varLib/featureVars_test.py123
-rw-r--r--Tests/varLib/mutator_test.py34
-rw-r--r--Tests/varLib/varLib_test.py94
-rw-r--r--[-rwxr-xr-x]fonttools0
-rwxr-xr-xpost_update.sh6
-rw-r--r--requirements.txt8
-rw-r--r--[-rwxr-xr-x]run-tests.sh0
-rw-r--r--setup.cfg6
-rw-r--r--[-rwxr-xr-x]setup.py17
-rw-r--r--tox.ini17
147 files changed, 10900 insertions, 1407 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
index e6d53c66..f4b2933c 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -3,12 +3,17 @@ environment:
- JOB: "2.7 32-bit"
PYTHON_HOME: "C:\\Python27"
- - JOB: "3.6 64-bit"
- PYTHON_HOME: "C:\\Python36-x64"
-
- JOB: "3.7 64-bit"
PYTHON_HOME: "C:\\Python37-x64"
+branches:
+ only:
+ - master
+ # We want to build wip/* branches since these are not usually used for PRs
+ - /^wip\/.*$/
+ # We want to build version tags as well.
+ - /^\d+\.\d+.*$/
+
install:
# If there is a newer build queued for the same PR, cancel this one.
# The AppVeyor 'rollout builds' option is supposed to serve the same
diff --git a/.travis.yml b/.travis.yml
index 4223e959..0951eaf2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,14 @@ env:
- TWINE_USERNAME="anthrotype"
- secure: PJuCmlDuwnojiw3QuDhfNAaU4f/yeJcEcRzJAudA66bwZK7hvxV7Tiy9A17Bm6yO0HbJmmyjsIr8h2e7/PyY6QCaV8RqcMDkQ0UraU16pRsihp0giVXJoWscj2sCP4cNDOBVwSaGAX8yZ2OONc5srESywghzcy8xmgw6O+XFqx4=
+branches:
+ only:
+ - master
+ # We want to build wip/* branches since these are not usually used for PRs
+ - /^wip\/.*$/
+ # We want to build version tags as well.
+ - /^\d+\.\d+.*$/
+
matrix:
fast_finish: true
exclude:
@@ -18,7 +26,7 @@ matrix:
env: TOXENV=py35-cov
- python: 3.6
env:
- - TOXENV=py36-cov
+ - TOXENV=py36-cov,package_readme
- BUILD_DIST=true
- python: 3.7
env: TOXENV=py37-cov
diff --git a/.travis/after_success.sh b/.travis/after_success.sh
index 07bcab5e..07bcab5e 100755..100644
--- 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 100755..100644
--- a/.travis/before_install.sh
+++ b/.travis/before_install.sh
diff --git a/.travis/install.sh b/.travis/install.sh
index f2a0717f..f2a0717f 100755..100644
--- a/.travis/install.sh
+++ b/.travis/install.sh
diff --git a/.travis/run.sh b/.travis/run.sh
index c6c1fea9..ffb0ef79 100755..100644
--- a/.travis/run.sh
+++ b/.travis/run.sh
@@ -11,4 +11,10 @@ tox
# re-run all the XML-related tests, this time without lxml but using the
# built-in ElementTree library.
-tox -e ${TOXENV:-py}-nolxml -- Tests/ufoLib Tests/misc/etree_test.py Tests/misc/plistlib_test.py
+if [ -z "$TOXENV" ]; then
+ TOXENV="py-nolxml"
+else
+ # strip additional tox envs after the comma, add -nolxml factor
+ TOXENV="${TOXENV%,*}-nolxml"
+fi
+tox -e $TOXENV -- Tests/ufoLib Tests/misc/etree_test.py Tests/misc/plistlib_test.py
diff --git a/Doc/source/designspaceLib/readme.rst b/Doc/source/designspaceLib/readme.rst
index 5af5d636..06bf46ff 100644
--- a/Doc/source/designspaceLib/readme.rst
+++ b/Doc/source/designspaceLib/readme.rst
@@ -278,16 +278,17 @@ AxisDescriptor object
- ``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.
-- ``minimum``: number. The minimum value for this axis. MutatorMath +
- Varlib.
-- ``maximum``: number. The maximum value for this axis. MutatorMath +
- Varlib.
-- ``default``: number. The default value for this axis, i.e. when a new
- location is created, this is the value this axis will get.
+- ``minimum``: number. The minimum value for this axis in user space.
MutatorMath + Varlib.
-- ``map``: list of input / output values that can describe a warp of
- user space to designspace coordinates. If no map values are present,
- it is assumed it is [(minimum, minimum), (maximum, maximum)]. Varlib.
+- ``maximum``: number. The maximum value for this axis in user space.
+ MutatorMath + Varlib.
+- ``default``: number. The default value for this axis, i.e. when a new
+ location is created, this is the value this axis will get in user
+ space. MutatorMath + Varlib.
+- ``map``: list of input / output values that can describe a warp of user space
+ to design space coordinates. If no map values are present, it is assumed user
+ space is the same as design space, as in [(minimum, minimum), (maximum, maximum)].
+ Varlib.
.. code:: python
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 10eab303..e922c48e 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.31.0"
+version = __version__ = "3.35.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py
index 83526ed5..4ad0e442 100644
--- a/Lib/fontTools/cffLib/__init__.py
+++ b/Lib/fontTools/cffLib/__init__.py
@@ -623,11 +623,6 @@ class GlobalSubrsIndex(Index):
self.fdSelect = fdSelect
if fdArray:
self.fdArray = fdArray
- if isCFF2:
- # CFF2Subr's can have numeric arguments on the stack after the last operator.
- self.subrClass = psCharStrings.CFF2Subr
- self.charStringClass = psCharStrings.CFF2Subr
-
def produceItem(self, index, data, file, offset):
if self.private is not None:
@@ -2336,7 +2331,7 @@ class TopDict(BaseDict):
def decompileAllCharStrings(self):
# Make sure that all the Private Dicts have been instantiated.
- for charString in self.CharStrings.values():
+ for i, charString in enumerate(self.CharStrings.values()):
try:
charString.decompile()
except:
@@ -2424,10 +2419,19 @@ class PrivateDict(BaseDict):
if isCFF2:
self.defaults = buildDefaults(privateDictOperators2)
self.order = buildOrder(privateDictOperators2)
+ # Provide dummy values. This avoids needing to provide
+ # an isCFF2 state in a lot of places.
+ self.nominalWidthX = self.defaultWidthX = None
else:
self.defaults = buildDefaults(privateDictOperators)
self.order = buildOrder(privateDictOperators)
+ def __getattr__(self, name):
+ if name == "in_cff2":
+ return self._isCFF2
+ value = BaseDict.__getattr__(self, name)
+ return value
+
def getNumRegions(self, vi=None): # called from misc/psCharStrings.py
# if getNumRegions is being called, we can assume that VarStore exists.
if vi is None:
diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py
index 5a6942d3..caf8c3b3 100644
--- a/Lib/fontTools/cffLib/specializer.py
+++ b/Lib/fontTools/cffLib/specializer.py
@@ -21,6 +21,7 @@ def stringToProgram(string):
program.append(token)
return program
+
def programToString(program):
return ' '.join(str(x) for x in program)
@@ -62,6 +63,7 @@ def programToCommands(program):
commands.append(('', stack))
return commands
+
def commandsToProgram(commands):
"""Takes a commands list as returned by programToCommands() and converts
it back to a T2CharString program list."""
@@ -415,7 +417,9 @@ def specializeCommands(commands,
continue
# Merge adjacent hlineto's and vlineto's.
- if i and op in {'hlineto', 'vlineto'} and op == commands[i-1][0]:
+ if (i and op in {'hlineto', 'vlineto'} and
+ (op == commands[i-1][0]) and
+ (not isinstance(args[0], list))):
_, other_args = commands[i-1]
assert len(args) == 1 and len(other_args) == 1
commands[i-1] = (op, [other_args[0]+args[0]])
diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py
index 24c2247f..d9da48c0 100644
--- a/Lib/fontTools/designspaceLib/__init__.py
+++ b/Lib/fontTools/designspaceLib/__init__.py
@@ -673,12 +673,6 @@ class BaseDocReader(LogMixin):
self.readInstances()
self.readLib()
- def getSourcePaths(self, makeGlyphs=True, makeKerning=True, makeInfo=True):
- paths = []
- for name in self.documentObject.sources.keys():
- paths.append(self.documentObject.sources[name][0].path)
- return paths
-
def readRules(self):
# we also need to read any conditions that are outside of a condition set.
rules = []
@@ -1053,6 +1047,8 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
return f.getvalue()
def read(self, path):
+ if hasattr(path, "__fspath__"): # support os.PathLike objects
+ path = path.__fspath__()
self.path = path
self.filename = os.path.basename(path)
reader = self.readerClass(path, self)
@@ -1061,6 +1057,8 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
self.findDefault()
def write(self, path):
+ if hasattr(path, "__fspath__"): # support os.PathLike objects
+ path = path.__fspath__()
self.path = path
self.filename = os.path.basename(path)
self.updatePaths()
@@ -1113,21 +1111,11 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
"""
+ assert self.path is not None
for descriptor in self.sources + self.instances:
- # check what the relative path really should be?
- expectedFilename = None
- if descriptor.path is not None and self.path is not None:
- expectedFilename = self._posixRelativePath(descriptor.path)
-
- # 3
- if descriptor.filename is None and descriptor.path is not None and self.path is not None:
+ if descriptor.path is not None:
+ # case 3 and 4: filename gets updated and relativized
descriptor.filename = self._posixRelativePath(descriptor.path)
- continue
-
- # 4
- if descriptor.filename is not None and descriptor.path is not None and self.path is not None:
- if descriptor.filename is not expectedFilename:
- descriptor.filename = expectedFilename
def addSource(self, sourceDescriptor):
self.sources.append(sourceDescriptor)
diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py
new file mode 100644
index 00000000..70519822
--- /dev/null
+++ b/Lib/fontTools/fontBuilder.py
@@ -0,0 +1,765 @@
+from __future__ import print_function, division, absolute_import
+from __future__ import unicode_literals
+
+__all__ = ["FontBuilder"]
+
+"""
+This module is *experimental*, meaning it still may evolve and change.
+
+The `FontBuilder` class is a convenient helper to construct working TTF or
+OTF fonts from scratch.
+
+Note that the various setup methods cannot be called in arbitrary order,
+due to various interdependencies between OpenType tables. Here is an order
+that works:
+
+ fb = FontBuilder(...)
+ fb.setupGlyphOrder(...)
+ fb.setupCharacterMap(...)
+ fb.setupGlyf(...) --or-- fb.setupCFF(...)
+ fb.setupHorizontalMetrics(...)
+ fb.setupHorizontalHeader()
+ fb.setupNameTable(...)
+ fb.setupOS2()
+ fb.setupPost()
+ fb.save(...)
+
+Here is how to build a minimal TTF:
+
+```python
+from fontTools.fontBuilder import FontBuilder
+from fontTools.pens.ttGlyphPen import TTGlyphPen
+
+def drawTestGlyph(pen):
+ pen.moveTo((100, 100))
+ pen.lineTo((100, 1000))
+ pen.qCurveTo((200, 900), (400, 900), (500, 1000))
+ pen.lineTo((500, 100))
+ pen.closePath()
+
+fb = FontBuilder(1024, isTTF=True)
+fb.setupGlyphOrder([".notdef", ".null", "A", "a"])
+fb.setupCharacterMap({65: "A", 97: "a"})
+
+advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}
+
+familyName = "HelloTestFont"
+styleName = "TotallyNormal"
+nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
+ styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
+nameStrings['psName'] = familyName + "-" + styleName
+
+pen = TTGlyphPen(None)
+drawTestGlyph(pen)
+glyph = pen.glyph()
+glyphs = {".notdef": glyph, "A": glyph, "a": glyph, ".null": glyph}
+fb.setupGlyf(glyphs)
+
+metrics = {}
+glyphTable = fb.font["glyf"]
+for gn, advanceWidth in advanceWidths.items():
+ metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
+fb.setupHorizontalMetrics(metrics)
+
+fb.setupHorizontalHeader(ascent=824, descent=200)
+fb.setupNameTable(nameStrings)
+fb.setupOS2()
+fb.setupPost()
+
+fb.save("test.ttf")
+```
+
+And here's how to build a minimal OTF:
+
+```python
+from fontTools.fontBuilder import FontBuilder
+from fontTools.pens.t2CharStringPen import T2CharStringPen
+
+def drawTestGlyph(pen):
+ pen.moveTo((100, 100))
+ pen.lineTo((100, 1000))
+ pen.curveTo((200, 900), (400, 900), (500, 1000))
+ pen.lineTo((500, 100))
+ pen.closePath()
+
+fb = FontBuilder(1024, isTTF=False)
+fb.setupGlyphOrder([".notdef", ".null", "A", "a"])
+fb.setupCharacterMap({65: "A", 97: "a"})
+
+advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}
+
+familyName = "HelloTestFont"
+styleName = "TotallyNormal"
+nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
+ styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
+nameStrings['psName'] = familyName + "-" + styleName
+
+pen = T2CharStringPen(600, None)
+drawTestGlyph(pen)
+charString = pen.getCharString()
+charStrings = {".notdef": charString, "A": charString, "a": charString, ".null": charString}
+fb.setupCFF(nameStrings['psName'], {"FullName": nameStrings['psName']}, charStrings, {})
+
+metrics = {}
+for gn, advanceWidth in advanceWidths.items():
+ metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph
+fb.setupHorizontalMetrics(metrics)
+
+fb.setupHorizontalHeader(ascent=824, descent=200)
+fb.setupNameTable(nameStrings)
+fb.setupOS2()
+fb.setupPost()
+
+fb.save("test.otf")
+```
+"""
+
+from .misc.py23 import *
+from .ttLib import TTFont, newTable
+from .ttLib.tables._c_m_a_p import cmap_classes
+from .ttLib.tables._n_a_m_e import NameRecord, makeName
+from .misc.timeTools import timestampNow
+import struct
+
+
+_headDefaults = dict(
+ tableVersion = 1.0,
+ fontRevision = 1.0,
+ checkSumAdjustment = 0,
+ magicNumber = 0x5F0F3CF5,
+ flags = 0x0003,
+ unitsPerEm = 1000,
+ created = 0,
+ modified = 0,
+ xMin = 0,
+ yMin = 0,
+ xMax = 0,
+ yMax = 0,
+ macStyle = 0,
+ lowestRecPPEM = 3,
+ fontDirectionHint = 2,
+ indexToLocFormat = 0,
+ glyphDataFormat = 0,
+)
+
+_maxpDefaultsTTF = dict(
+ tableVersion = 0x00010000,
+ numGlyphs = 0,
+ maxPoints = 0,
+ maxContours = 0,
+ maxCompositePoints = 0,
+ maxCompositeContours = 0,
+ maxZones = 2,
+ maxTwilightPoints = 0,
+ maxStorage = 0,
+ maxFunctionDefs = 0,
+ maxInstructionDefs = 0,
+ maxStackElements = 0,
+ maxSizeOfInstructions = 0,
+ maxComponentElements = 0,
+ maxComponentDepth = 0,
+)
+_maxpDefaultsOTF = dict(
+ tableVersion = 0x00005000,
+ numGlyphs = 0,
+)
+
+_postDefaults = dict(
+ formatType = 3.0,
+ italicAngle = 0,
+ underlinePosition = 0,
+ underlineThickness = 0,
+ isFixedPitch = 0,
+ minMemType42 = 0,
+ maxMemType42 = 0,
+ minMemType1 = 0,
+ maxMemType1 = 0,
+)
+
+_hheaDefaults = dict(
+ tableVersion = 0x00010000,
+ ascent = 0,
+ descent = 0,
+ lineGap = 0,
+ advanceWidthMax = 0,
+ minLeftSideBearing = 0,
+ minRightSideBearing = 0,
+ xMaxExtent = 0,
+ caretSlopeRise = 1,
+ caretSlopeRun = 0,
+ caretOffset = 0,
+ reserved0 = 0,
+ reserved1 = 0,
+ reserved2 = 0,
+ reserved3 = 0,
+ metricDataFormat = 0,
+ numberOfHMetrics = 0,
+)
+
+_vheaDefaults = dict(
+ tableVersion = 0x00010000,
+ ascent = 0,
+ descent = 0,
+ lineGap = 0,
+ advanceHeightMax = 0,
+ minTopSideBearing = 0,
+ minBottomSideBearing = 0,
+ yMaxExtent = 0,
+ caretSlopeRise = 0,
+ caretSlopeRun = 0,
+ reserved0 = 0,
+ reserved1 = 0,
+ reserved2 = 0,
+ reserved3 = 0,
+ reserved4 = 0,
+ metricDataFormat = 0,
+ numberOfVMetrics = 0,
+)
+
+_nameIDs = dict(
+ copyright = 0,
+ familyName = 1,
+ styleName = 2,
+ uniqueFontIdentifier = 3,
+ fullName = 4,
+ version = 5,
+ psName = 6,
+ trademark = 7,
+ manufacturer = 8,
+ designer = 9,
+ description = 10,
+ vendorURL = 11,
+ designerURL = 12,
+ licenseDescription = 13,
+ licenseInfoURL = 14,
+ # reserved = 15,
+ typographicFamily = 16,
+ typographicSubfamily = 17,
+ compatibleFullName = 18,
+ sampleText = 19,
+ postScriptCIDFindfontName = 20,
+ wwsFamilyName = 21,
+ wwsSubfamilyName = 22,
+ lightBackgroundPalette = 23,
+ darkBackgroundPalette = 24,
+ variationsPostScriptNamePrefix = 25,
+)
+
+# to insert in setupNameTable doc string:
+# print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1])))
+
+_panoseDefaults = dict(
+ bFamilyType = 0,
+ bSerifStyle = 0,
+ bWeight = 0,
+ bProportion = 0,
+ bContrast = 0,
+ bStrokeVariation = 0,
+ bArmStyle = 0,
+ bLetterForm = 0,
+ bMidline = 0,
+ bXHeight = 0,
+)
+
+_OS2Defaults = dict(
+ version = 3,
+ xAvgCharWidth = 0,
+ usWeightClass = 400,
+ usWidthClass = 5,
+ fsType = 0x0004, # default: Preview & Print embedding
+ ySubscriptXSize = 0,
+ ySubscriptYSize = 0,
+ ySubscriptXOffset = 0,
+ ySubscriptYOffset = 0,
+ ySuperscriptXSize = 0,
+ ySuperscriptYSize = 0,
+ ySuperscriptXOffset = 0,
+ ySuperscriptYOffset = 0,
+ yStrikeoutSize = 0,
+ yStrikeoutPosition = 0,
+ sFamilyClass = 0,
+ panose = _panoseDefaults,
+ ulUnicodeRange1 = 0,
+ ulUnicodeRange2 = 0,
+ ulUnicodeRange3 = 0,
+ ulUnicodeRange4 = 0,
+ achVendID = "????",
+ fsSelection = 0,
+ usFirstCharIndex = 0,
+ usLastCharIndex = 0,
+ sTypoAscender = 0,
+ sTypoDescender = 0,
+ sTypoLineGap = 0,
+ usWinAscent = 0,
+ usWinDescent = 0,
+ ulCodePageRange1 = 0,
+ ulCodePageRange2 = 0,
+ sxHeight = 0,
+ sCapHeight = 0,
+ usDefaultChar = 0, # .notdef
+ usBreakChar = 32, # space
+ usMaxContext = 2, # just kerning
+ usLowerOpticalPointSize = 0,
+ usUpperOpticalPointSize = 0,
+)
+
+
+class FontBuilder(object):
+
+ def __init__(self, unitsPerEm=None, font=None, isTTF=True):
+ """Initialize a FontBuilder instance.
+
+ If the `font` argument is not given, a new `TTFont` will be
+ constructed, and `unitsPerEm` must be given. If `isTTF` is True,
+ the font will be a glyf-based TTF; if `isTTF` is False it will be
+ a CFF-based OTF.
+
+ If `font` is given, it must be a `TTFont` instance and `unitsPerEm`
+ must _not_ be given. The `isTTF` argument will be ignored.
+ """
+ if font is None:
+ self.font = TTFont(recalcTimestamp=False)
+ self.isTTF = isTTF
+ now = timestampNow()
+ assert unitsPerEm is not None
+ self.setupHead(unitsPerEm=unitsPerEm, created=now, modified=now)
+ self.setupMaxp()
+ else:
+ assert unitsPerEm is None
+ self.font = font
+ self.isTTF = "glyf" in font
+
+ def save(self, file):
+ """Save the font. The 'file' argument can be either a pathname or a
+ writable file object.
+ """
+ self.font.save(file)
+
+ def _initTableWithValues(self, tableTag, defaults, values):
+ table = self.font[tableTag] = newTable(tableTag)
+ for k, v in defaults.items():
+ setattr(table, k, v)
+ for k, v in values.items():
+ setattr(table, k, v)
+ return table
+
+ def _updateTableWithValues(self, tableTag, values):
+ table = self.font[tableTag]
+ for k, v in values.items():
+ setattr(table, k, v)
+
+ def setupHead(self, **values):
+ """Create a new `head` table and initialize it with default values,
+ which can be overridden by keyword arguments.
+ """
+ self._initTableWithValues("head", _headDefaults, values)
+
+ def updateHead(self, **values):
+ """Update the head table with the fields and values passed as
+ keyword arguments.
+ """
+ self._updateTableWithValues("head", values)
+
+ def setupGlyphOrder(self, glyphOrder):
+ """Set the glyph order for the font."""
+ self.font.setGlyphOrder(glyphOrder)
+
+ def setupCharacterMap(self, cmapping, 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.
+ """
+ subTables = []
+ highestUnicode = max(cmapping)
+ if highestUnicode > 0xffff:
+ cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000)
+ subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10)
+ subTables.append(subTable_3_10)
+ else:
+ cmapping_3_1 = cmapping
+ format = 4
+ subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
+ try:
+ subTable_3_1.compile(self.font)
+ except struct.error:
+ # format 4 overflowed, fall back to format 12
+ if not allowFallback:
+ raise ValueError("cmap format 4 subtable overflowed; sort glyph order by unicode to fix.")
+ format = 12
+ subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
+ subTables.append(subTable_3_1)
+ subTable_0_3 = buildCmapSubTable(cmapping_3_1, format, 0, 3)
+ subTables.append(subTable_0_3)
+
+ self.font["cmap"] = newTable("cmap")
+ self.font["cmap"].tableVersion = 0
+ self.font["cmap"].tables = subTables
+
+ def setupNameTable(self, nameStrings, windows=True, mac=True):
+ """Create the `name` table for the font. The `nameStrings` argument must
+ be a dict, mapping nameIDs or descriptive names for the nameIDs to name
+ record values. A value is either a string, or a dict, mapping language codes
+ to strings, to allow localized name table entries.
+
+ By default, both Windows (platformID=3) and Macintosh (platformID=1) name
+ records are added, unless any of `windows` or `mac` arguments is False.
+
+ The following descriptive names are available for nameIDs:
+
+ copyright (nameID 0)
+ familyName (nameID 1)
+ styleName (nameID 2)
+ uniqueFontIdentifier (nameID 3)
+ fullName (nameID 4)
+ version (nameID 5)
+ psName (nameID 6)
+ trademark (nameID 7)
+ manufacturer (nameID 8)
+ designer (nameID 9)
+ description (nameID 10)
+ vendorURL (nameID 11)
+ designerURL (nameID 12)
+ licenseDescription (nameID 13)
+ licenseInfoURL (nameID 14)
+ typographicFamily (nameID 16)
+ typographicSubfamily (nameID 17)
+ compatibleFullName (nameID 18)
+ sampleText (nameID 19)
+ postScriptCIDFindfontName (nameID 20)
+ wwsFamilyName (nameID 21)
+ wwsSubfamilyName (nameID 22)
+ lightBackgroundPalette (nameID 23)
+ darkBackgroundPalette (nameID 24)
+ variationsPostScriptNamePrefix (nameID 25)
+ """
+ nameTable = self.font["name"] = newTable("name")
+ nameTable.names = []
+
+ for nameName, nameValue in nameStrings.items():
+ if isinstance(nameName, int):
+ nameID = nameName
+ else:
+ nameID = _nameIDs[nameName]
+ if isinstance(nameValue, basestring):
+ nameValue = dict(en=nameValue)
+ nameTable.addMultilingualName(
+ nameValue, ttFont=self.font, nameID=nameID, windows=windows, mac=mac
+ )
+
+ def setupOS2(self, **values):
+ """Create a new `OS/2` table and initialize it with default values,
+ which can be overridden by keyword arguments.
+ """
+ if "xAvgCharWidth" not in values:
+ gs = self.font.getGlyphSet()
+ widths = [gs[glyphName].width for glyphName in gs.keys() if gs[glyphName].width > 0]
+ values["xAvgCharWidth"] = int(round(sum(widths) / float(len(widths))))
+ self._initTableWithValues("OS/2", _OS2Defaults, values)
+ if not ("ulUnicodeRange1" in values or "ulUnicodeRange2" in values or
+ "ulUnicodeRange3" in values or "ulUnicodeRange3" in values):
+ assert "cmap" in self.font, "the 'cmap' table must be setup before the 'OS/2' table"
+ self.font["OS/2"].recalcUnicodeRanges(self.font)
+
+ def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
+ from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \
+ GlobalSubrsIndex, PrivateDict
+
+ assert not self.isTTF
+ self.font.sfntVersion = "OTTO"
+ fontSet = CFFFontSet()
+ fontSet.major = 1
+ fontSet.minor = 0
+ fontSet.fontNames = [psName]
+ fontSet.topDictIndex = TopDictIndex()
+
+ globalSubrs = GlobalSubrsIndex()
+ fontSet.GlobalSubrs = globalSubrs
+ private = PrivateDict()
+ for key, value in privateDict.items():
+ setattr(private, key, value)
+ fdSelect = None
+ fdArray = None
+
+ topDict = TopDict()
+ topDict.charset = self.font.getGlyphOrder()
+ topDict.Private = private
+ for key, value in fontInfo.items():
+ setattr(topDict, key, value)
+ if "FontMatrix" not in fontInfo:
+ scale = 1 / self.font["head"].unitsPerEm
+ topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
+
+ charStrings = CharStrings(None, topDict.charset, globalSubrs, private, fdSelect, fdArray)
+ for glyphName, charString in charStringsDict.items():
+ charString.private = private
+ charString.globalSubrs = globalSubrs
+ charStrings[glyphName] = charString
+ topDict.CharStrings = charStrings
+
+ fontSet.topDictIndex.append(topDict)
+
+ self.font["CFF "] = newTable("CFF ")
+ self.font["CFF "].cff = fontSet
+
+ def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None):
+ from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \
+ GlobalSubrsIndex, PrivateDict, FDArrayIndex, FontDict
+
+ assert not self.isTTF
+ self.font.sfntVersion = "OTTO"
+ fontSet = CFFFontSet()
+ fontSet.major = 2
+ fontSet.minor = 0
+
+ cff2GetGlyphOrder = self.font.getGlyphOrder
+ fontSet.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None)
+
+ globalSubrs = GlobalSubrsIndex()
+ fontSet.GlobalSubrs = globalSubrs
+
+ if fdArrayList is None:
+ fdArrayList = [{}]
+ fdSelect = None
+ fdArray = FDArrayIndex()
+ fdArray.strings = None
+ fdArray.GlobalSubrs = globalSubrs
+ for privateDict in fdArrayList:
+ fontDict = FontDict()
+ fontDict.setCFF2(True)
+ private = PrivateDict()
+ for key, value in privateDict.items():
+ setattr(private, key, value)
+ fontDict.Private = private
+ fdArray.append(fontDict)
+
+ topDict = TopDict()
+ topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
+ topDict.FDArray = fdArray
+ scale = 1 / self.font["head"].unitsPerEm
+ topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
+
+ private = fdArray[0].Private
+ charStrings = CharStrings(None, None, globalSubrs, private, fdSelect, fdArray)
+ for glyphName, charString in charStringsDict.items():
+ charString.private = private
+ charString.globalSubrs = globalSubrs
+ charStrings[glyphName] = charString
+ topDict.CharStrings = charStrings
+
+ fontSet.topDictIndex.append(topDict)
+
+ self.font["CFF2"] = newTable("CFF2")
+ self.font["CFF2"].cff = fontSet
+
+ if regions:
+ self.setupCFF2Regions(regions)
+
+ def setupCFF2Regions(self, regions):
+ from .varLib.builder import buildVarRegionList, buildVarData, buildVarStore
+ from .cffLib import VarStoreData
+
+ assert "fvar" in self.font, "fvar must to be set up first"
+ assert "CFF2" in self.font, "CFF2 must to be set up first"
+ axisTags = [a.axisTag for a in self.font["fvar"].axes]
+ varRegionList = buildVarRegionList(regions, axisTags)
+ varData = buildVarData(list(range(len(regions))), None, optimize=False)
+ varStore = buildVarStore(varRegionList, [varData])
+ vstore = VarStoreData(otVarStore=varStore)
+ self.font["CFF2"].cff.topDictIndex[0].VarStore = vstore
+
+ def setupGlyf(self, glyphs, calcGlyphBounds=True):
+ """Create the `glyf` table from a dict, that maps glyph names
+ to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example
+ as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`.
+
+ If `calcGlyphBounds` is True, the bounds of all glyphs will be
+ calculated. Only pass False if your glyph objects already have
+ their bounding box values set.
+ """
+ assert self.isTTF
+ self.font["loca"] = newTable("loca")
+ self.font["glyf"] = newTable("glyf")
+ self.font["glyf"].glyphs = glyphs
+ if hasattr(self.font, "glyphOrder"):
+ self.font["glyf"].glyphOrder = self.font.glyphOrder
+ if calcGlyphBounds:
+ self.calcGlyphBounds()
+
+ def setupFvar(self, axes, instances):
+ addFvar(self.font, axes, instances)
+
+ def setupGvar(self, variations):
+ gvar = self.font["gvar"] = newTable('gvar')
+ gvar.version = 1
+ gvar.reserved = 0
+ gvar.variations = variations
+
+ def calcGlyphBounds(self):
+ """Calculate the bounding boxes of all glyphs in the `glyf` table.
+ This is usually not called explicitly by client code.
+ """
+ glyphTable = self.font["glyf"]
+ for glyph in glyphTable.glyphs.values():
+ glyph.recalcBounds(glyphTable)
+
+ def setupHorizontalMetrics(self, metrics):
+ """Create a new `hmtx` table, for horizontal metrics.
+
+ The `metrics` argument must be a dict, mapping glyph names to
+ `(width, leftSidebearing)` tuples.
+ """
+ self.setupMetrics('hmtx', metrics)
+
+ def setupVerticalMetrics(self, metrics):
+ """Create a new `vmtx` table, for horizontal metrics.
+
+ The `metrics` argument must be a dict, mapping glyph names to
+ `(height, topSidebearing)` tuples.
+ """
+ self.setupMetrics('vmtx', metrics)
+
+ def setupMetrics(self, tableTag, metrics):
+ """See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
+ assert tableTag in ("hmtx", "vmtx")
+ mtxTable = self.font[tableTag] = newTable(tableTag)
+ roundedMetrics = {}
+ for gn in metrics:
+ w, lsb = metrics[gn]
+ roundedMetrics[gn] = int(round(w)), int(round(lsb))
+ mtxTable.metrics = roundedMetrics
+
+ def setupHorizontalHeader(self, **values):
+ """Create a new `hhea` table initialize it with default values,
+ which can be overridden by keyword arguments.
+ """
+ self._initTableWithValues("hhea", _hheaDefaults, values)
+
+ def setupVerticalHeader(self, **values):
+ """Create a new `vhea` table initialize it with default values,
+ which can be overridden by keyword arguments.
+ """
+ self._initTableWithValues("vhea", _vheaDefaults, values)
+
+ def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None):
+ """Create a new `VORG` table. The `verticalOrigins` argument must be
+ a dict, mapping glyph names to vertical origin values.
+
+ The `defaultVerticalOrigin` argument should be the most common vertical
+ origin value. If omitted, this value will be derived from the actual
+ values in the `verticalOrigins` argument.
+ """
+ if defaultVerticalOrigin is None:
+ # find the most frequent vorg value
+ bag = {}
+ for gn in verticalOrigins:
+ vorg = verticalOrigins[gn]
+ if vorg not in bag:
+ bag[vorg] = 1
+ else:
+ bag[vorg] += 1
+ defaultVerticalOrigin = sorted(bag, key=lambda vorg: bag[vorg], reverse=True)[0]
+ self._initTableWithValues("VORG", {}, dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin))
+ vorgTable = self.font["VORG"]
+ vorgTable.majorVersion = 1
+ vorgTable.minorVersion = 0
+ for gn in verticalOrigins:
+ vorgTable[gn] = verticalOrigins[gn]
+
+ def setupPost(self, keepGlyphNames=True, **values):
+ """Create a new `post` table and initialize it with default values,
+ which can be overridden by keyword arguments.
+ """
+ postTable = self._initTableWithValues("post", _postDefaults, values)
+ if self.isTTF and keepGlyphNames:
+ postTable.formatType = 2.0
+ postTable.extraNames = []
+ postTable.mapping = {}
+ else:
+ postTable.formatType = 3.0
+
+ def setupMaxp(self):
+ """Create a new `maxp` table. This is called implicitly by FontBuilder
+ itself and is usually not called by client code.
+ """
+ if self.isTTF:
+ defaults = _maxpDefaultsTTF
+ else:
+ defaults = _maxpDefaultsOTF
+ self._initTableWithValues("maxp", defaults, {})
+
+ def setupDummyDSIG(self):
+ """This adds a dummy DSIG table to the font to make some MS applications
+ happy. This does not properly sign the font.
+ """
+ from .ttLib.tables.D_S_I_G_ import SignatureRecord
+
+ sig = SignatureRecord()
+ sig.ulLength = 20
+ sig.cbSignature = 12
+ sig.usReserved2 = 0
+ sig.usReserved1 = 0
+ sig.pkcs7 = b'\xd3M4\xd3M5\xd3M4\xd3M4'
+ sig.ulFormat = 1
+ sig.ulOffset = 20
+
+ values = dict(
+ ulVersion = 1,
+ usFlag = 1,
+ usNumSigs = 1,
+ signatureRecords = [sig],
+ )
+ self._initTableWithValues("DSIG", {}, values)
+
+ def addOpenTypeFeatures(self, features, filename=None, tables=None):
+ """Add OpenType features to the font from a string containing
+ Feature File syntax.
+
+ The `filename` argument is used in error messages and to determine
+ where to look for "include" files.
+
+ The optional `tables` argument can be a list of OTL tables tags to
+ build, allowing the caller to only build selected OTL tables. See
+ `fontTools.feaLib` for details.
+ """
+ from .feaLib.builder import addOpenTypeFeaturesFromString
+ addOpenTypeFeaturesFromString(self.font, features, filename=filename, tables=tables)
+
+
+def buildCmapSubTable(cmapping, format, platformID, platEncID):
+ subTable = cmap_classes[format](format)
+ subTable.cmap = cmapping
+ subTable.platformID = platformID
+ subTable.platEncID = platEncID
+ subTable.language = 0
+ return subTable
+
+
+def addFvar(font, axes, instances):
+ from .misc.py23 import Tag, tounicode
+ from .ttLib.tables._f_v_a_r import Axis, NamedInstance
+
+ assert axes
+
+ fvar = newTable('fvar')
+ nameTable = font['name']
+
+ for tag, minValue, defaultValue, maxValue, name in axes:
+ axis = Axis()
+ axis.axisTag = Tag(tag)
+ axis.minValue, axis.defaultValue, axis.maxValue = minValue, defaultValue, maxValue
+ axis.axisNameID = nameTable.addName(tounicode(name))
+ fvar.axes.append(axis)
+
+ for instance in instances:
+ coordinates = instance['location']
+ name = tounicode(instance['stylename'])
+ psname = instance.get('postscriptfontname')
+
+ inst = NamedInstance()
+ inst.subfamilyNameID = nameTable.addName(name)
+ if psname is not None:
+ psname = tounicode(psname)
+ inst.postscriptNameID = nameTable.addName(psname)
+ inst.coordinates = coordinates
+ fvar.instances.append(inst)
+
+ font['fvar'] = fvar
diff --git a/Lib/fontTools/merge.py b/Lib/fontTools/merge.py
index 7dcb8b52..1005b859 100644
--- a/Lib/fontTools/merge.py
+++ b/Lib/fontTools/merge.py
@@ -498,7 +498,11 @@ def mergeScripts(lst):
self = otTables.Script()
self.LangSysRecord = lsrecords
self.LangSysCount = len(lsrecords)
- self.DefaultLangSys = mergeLangSyses([s.DefaultLangSys for s in lst if s.DefaultLangSys])
+ dfltLangSyses = [s.DefaultLangSys for s in lst if s.DefaultLangSys]
+ if dfltLangSyses:
+ self.DefaultLangSys = mergeLangSyses(dfltLangSyses)
+ else:
+ self.DefaultLangSys = None
return self
def mergeScriptRecords(lst):
diff --git a/Lib/fontTools/misc/dictTools.py b/Lib/fontTools/misc/dictTools.py
new file mode 100644
index 00000000..db5d6587
--- /dev/null
+++ b/Lib/fontTools/misc/dictTools.py
@@ -0,0 +1,68 @@
+"""Misc dict tools."""
+
+from __future__ import print_function, absolute_import, division
+from fontTools.misc.py23 import *
+
+__all__ = ['hashdict']
+
+# https://stackoverflow.com/questions/1151658/python-hashable-dicts
+class hashdict(dict):
+ """
+ hashable dict implementation, suitable for use as a key into
+ other dicts.
+
+ >>> h1 = hashdict({"apples": 1, "bananas":2})
+ >>> h2 = hashdict({"bananas": 3, "mangoes": 5})
+ >>> h1+h2
+ hashdict(apples=1, bananas=3, mangoes=5)
+ >>> d1 = {}
+ >>> d1[h1] = "salad"
+ >>> d1[h1]
+ 'salad'
+ >>> d1[h2]
+ Traceback (most recent call last):
+ ...
+ KeyError: hashdict(bananas=3, mangoes=5)
+
+ based on answers from
+ http://stackoverflow.com/questions/1151658/python-hashable-dicts
+
+ """
+ def __key(self):
+ return tuple(sorted(self.items()))
+ def __repr__(self):
+ return "{0}({1})".format(self.__class__.__name__,
+ ", ".join("{0}={1}".format(
+ str(i[0]),repr(i[1])) for i in self.__key()))
+
+ def __hash__(self):
+ return hash(self.__key())
+ def __setitem__(self, key, value):
+ raise TypeError("{0} does not support item assignment"
+ .format(self.__class__.__name__))
+ def __delitem__(self, key):
+ raise TypeError("{0} does not support item assignment"
+ .format(self.__class__.__name__))
+ def clear(self):
+ raise TypeError("{0} does not support item assignment"
+ .format(self.__class__.__name__))
+ def pop(self, *args, **kwargs):
+ raise TypeError("{0} does not support item assignment"
+ .format(self.__class__.__name__))
+ def popitem(self, *args, **kwargs):
+ raise TypeError("{0} does not support item assignment"
+ .format(self.__class__.__name__))
+ def setdefault(self, *args, **kwargs):
+ raise TypeError("{0} does not support item assignment"
+ .format(self.__class__.__name__))
+ def update(self, *args, **kwargs):
+ raise TypeError("{0} does not support item assignment"
+ .format(self.__class__.__name__))
+ # update is not ok because it mutates the object
+ # __add__ is ok because it creates a new object
+ # while the new object is under construction, it's ok to mutate it
+ def __add__(self, right):
+ result = hashdict(self)
+ dict.update(result, right)
+ return result
+
diff --git a/Lib/fontTools/misc/intTools.py b/Lib/fontTools/misc/intTools.py
new file mode 100644
index 00000000..9eb2f0f9
--- /dev/null
+++ b/Lib/fontTools/misc/intTools.py
@@ -0,0 +1,18 @@
+"""Misc integer tools."""
+
+from __future__ import print_function, absolute_import, division
+from fontTools.misc.py23 import *
+
+__all__ = ['popCount']
+
+
+def popCount(v):
+ """Return number of 1 bits in an integer."""
+
+ if v > 0xFFFFFFFF:
+ return popCount(v >> 32) + popCount(v & 0xFFFFFFFF)
+
+ # HACKMEM 169
+ y = (v >> 1) & 0xDB6DB6DB
+ y = v - y - ((y >> 1) & 0xDB6DB6DB)
+ return (((y + (y >> 3)) & 0xC71C71C7) % 0x3F)
diff --git a/Lib/fontTools/misc/macRes.py b/Lib/fontTools/misc/macRes.py
index 20bfa717..db832ec5 100644
--- a/Lib/fontTools/misc/macRes.py
+++ b/Lib/fontTools/misc/macRes.py
@@ -33,6 +33,8 @@ class ResourceReader(MutableMapping):
@staticmethod
def openResourceFork(path):
+ if hasattr(path, "__fspath__"): # support os.PathLike objects
+ path = path.__fspath__()
with open(path + '/..namedfork/rsrc', 'rb') as resfork:
data = resfork.read()
infile = BytesIO(data)
diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py
index fe695936..a0e1003f 100644
--- a/Lib/fontTools/misc/plistlib.py
+++ b/Lib/fontTools/misc/plistlib.py
@@ -7,6 +7,11 @@ from base64 import b64encode, b64decode
from numbers import Integral
try:
+ from collections.abc import Mapping # python >= 3.3
+except ImportError:
+ from collections import Mapping
+
+try:
from functools import singledispatch
except ImportError:
try:
@@ -384,7 +389,7 @@ if singledispatch is not None:
_make_element.register(bool)(_bool_element)
_make_element.register(Integral)(_integer_element)
_make_element.register(float)(_real_element)
- _make_element.register(dict)(_dict_element)
+ _make_element.register(Mapping)(_dict_element)
_make_element.register(list)(_array_element)
_make_element.register(tuple)(_array_element)
_make_element.register(datetime)(_date_element)
@@ -404,7 +409,7 @@ else:
return _integer_element(value, ctx)
elif isinstance(value, float):
return _real_element(value, ctx)
- elif isinstance(value, dict):
+ elif isinstance(value, Mapping):
return _dict_element(value, ctx)
elif isinstance(value, (list, tuple)):
return _array_element(value, ctx)
diff --git a/Lib/fontTools/misc/psCharStrings.py b/Lib/fontTools/misc/psCharStrings.py
index a92802b0..68810744 100644
--- a/Lib/fontTools/misc/psCharStrings.py
+++ b/Lib/fontTools/misc/psCharStrings.py
@@ -223,9 +223,16 @@ def encodeFixed(f, pack=struct.pack):
else:
return b"\xff" + pack(">l", value) # encode the entire fixed point value
+
+realZeroBytes = bytechr(30) + bytechr(0xf)
+
def encodeFloat(f):
# For CFF only, used in cffLib
- s = str(f).upper()
+ if f == 0.0: # 0.0 == +0.0 == -0.0
+ return realZeroBytes
+ # Note: 14 decimal digits seems to be the limitation for CFF real numbers
+ # in macOS. However, we use 8 here to match the implementation of AFDKO.
+ s = "%.8G" % f
if s[:2] == "0.":
s = s[1:]
elif s[:3] == "-0.":
@@ -234,9 +241,13 @@ def encodeFloat(f):
while s:
c = s[0]
s = s[1:]
- if c == "E" and s[:1] == "-":
- s = s[1:]
- c = "E-"
+ if c == "E":
+ c2 = s[:1]
+ if c2 == "-":
+ s = s[1:]
+ c = "E-"
+ elif c2 == "+":
+ s = s[1:]
nibbles.append(realNibblesDict[c])
nibbles.append(0xf)
if len(nibbles) % 2:
@@ -267,22 +278,6 @@ class SimpleT2Decompiler(object):
self.hintMaskBytes = 0
self.numRegions = 0
- def check_program(self, program):
- if not hasattr(self, 'private') or self.private is None:
- # Type 1 charstrings don't have self.private.
- # Type2 CFF charstrings may have self.private == None.
- # In both cases, they are not CFF2 charstrings
- isCFF2 = False
- else:
- isCFF2 = self.private._isCFF2
- if isCFF2:
- if program:
- assert program[-1] not in ("seac",), "illegal CharString Terminator"
- else:
- assert program, "illegal CharString: decompiled to empty program"
- assert program[-1] in ("endchar", "return", "callsubr", "callgsubr",
- "seac"), "illegal CharString"
-
def execute(self, charString):
self.callingStack.append(charString)
needsDecompilation = charString.needsDecompilation()
@@ -311,7 +306,6 @@ class SimpleT2Decompiler(object):
else:
pushToStack(token)
if needsDecompilation:
- self.check_program(program)
charString.setProgram(program)
del self.callingStack[-1]
@@ -424,7 +418,7 @@ class SimpleT2Decompiler(object):
numBlends = self.pop()
numOps = numBlends * (self.numRegions + 1)
blendArgs = self.operandStack[-numOps:]
- del self.operandStack[:-(numOps-numBlends)] # Leave the default operands on the stack.
+ del self.operandStack[-(numOps-numBlends):] # Leave the default operands on the stack.
def op_vsindex(self, index):
vi = self.pop()
@@ -463,8 +457,8 @@ t1Operators = [
class T2WidthExtractor(SimpleT2Decompiler):
- def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX):
- SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs)
+ def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None):
+ SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
self.nominalWidthX = nominalWidthX
self.defaultWidthX = defaultWidthX
@@ -477,6 +471,8 @@ class T2WidthExtractor(SimpleT2Decompiler):
args = self.popall()
if not self.gotWidth:
if evenOdd ^ (len(args) % 2):
+ # For CFF2 charstrings, this should never happen
+ assert self.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value"
self.width = self.nominalWidthX + args[0]
args = args[1:]
else:
@@ -503,9 +499,9 @@ class T2WidthExtractor(SimpleT2Decompiler):
class T2OutlineExtractor(T2WidthExtractor):
- def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX):
+ def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None):
T2WidthExtractor.__init__(
- self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX)
+ self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private)
self.pen = pen
def reset(self):
@@ -705,12 +701,6 @@ class T2OutlineExtractor(T2WidthExtractor):
self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3))
self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6))
- #
- # MultipleMaster. Well...
- #
- def op_blend(self, index):
- self.popall()
-
# misc
def op_and(self, index):
raise NotImplementedError
@@ -947,7 +937,6 @@ class T2CharString(object):
operators, opcodes = buildOperatorDict(t2Operators)
decompilerClass = SimpleT2Decompiler
outlineExtractor = T2OutlineExtractor
- isCFF2 = False
def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None):
if program is None:
@@ -979,7 +968,8 @@ class T2CharString(object):
def draw(self, pen):
subrs = getattr(self.private, "Subrs", [])
extractor = self.outlineExtractor(pen, subrs, self.globalSubrs,
- self.private.nominalWidthX, self.private.defaultWidthX)
+ self.private.nominalWidthX, self.private.defaultWidthX,
+ self.private)
extractor.execute(self)
self.width = extractor.width
@@ -988,20 +978,11 @@ class T2CharString(object):
self.draw(boundsPen)
return boundsPen.bounds
- def check_program(self, program, isCFF2=False):
- if isCFF2:
- if self.program:
- assert self.program[-1] not in ("seac",), "illegal CFF2 CharString Termination"
- else:
- assert self.program, "illegal CharString: decompiled to empty program"
- assert self.program[-1] in ("endchar", "return", "callsubr", "callgsubr", "seac"), "illegal CharString"
-
def compile(self, isCFF2=False):
if self.bytecode is not None:
return
opcodes = self.opcodes
program = self.program
- self.check_program(program, isCFF2=isCFF2)
bytecode = []
encodeInt = self.getIntEncoder()
encodeFixed = self.getFixedEncoder()
@@ -1148,9 +1129,6 @@ class T2CharString(object):
program.append(token)
self.setProgram(program)
-class CFF2Subr(T2CharString):
- isCFF2 = True
-
class T1CharString(T2CharString):
operandEncoding = t1OperandEncoding
diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py
index d4f4880a..beec5cb5 100644
--- a/Lib/fontTools/mtiLib/__init__.py
+++ b/Lib/fontTools/mtiLib/__init__.py
@@ -1174,7 +1174,8 @@ def main(args=None, font=None):
del args[0]
for f in args:
log.debug("Processing %s", f)
- table = build(open(f, 'rt', encoding="utf-8"), font, tableTag=tableTag)
+ with open(f, 'rt', encoding="utf-8") as f:
+ table = build(f, font, tableTag=tableTag)
blob = table.compile(font) # Make sure it compiles
decompiled = table.__class__()
decompiled.decompile(blob, font) # Make sure it decompiles!
diff --git a/Lib/fontTools/pens/pointPen.py b/Lib/fontTools/pens/pointPen.py
index 641eb446..415972f6 100644
--- a/Lib/fontTools/pens/pointPen.py
+++ b/Lib/fontTools/pens/pointPen.py
@@ -61,7 +61,7 @@ class BasePointToSegmentPen(AbstractPointPen):
def __init__(self):
self.currentPath = None
- def beginPath(self, **kwargs):
+ def beginPath(self, identifier=None, **kwargs):
assert self.currentPath is None
self.currentPath = []
@@ -140,7 +140,8 @@ class BasePointToSegmentPen(AbstractPointPen):
self._flushContour(segments)
- def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None,
+ identifier=None, **kwargs):
self.currentPath.append((pt, segmentType, smooth, name, kwargs))
@@ -313,22 +314,29 @@ class GuessSmoothPointPen(AbstractPointPen):
for pt, segmentType, smooth, name, kwargs in points:
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
- def beginPath(self):
+ def beginPath(self, identifier=None, **kwargs):
assert self._points is None
self._points = []
- self._outPen.beginPath()
+ if identifier is not None:
+ kwargs["identifier"] = identifier
+ self._outPen.beginPath(**kwargs)
def endPath(self):
self._flushContour()
self._outPen.endPath()
self._points = None
- def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None,
+ identifier=None, **kwargs):
+ if identifier is not None:
+ kwargs["identifier"] = identifier
self._points.append((pt, segmentType, False, name, kwargs))
- def addComponent(self, glyphName, transformation):
+ def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
assert self._points is None
- self._outPen.addComponent(glyphName, transformation)
+ if identifier is not None:
+ kwargs["identifier"] = identifier
+ self._outPen.addComponent(glyphName, transformation, **kwargs)
class ReverseContourPointPen(AbstractPointPen):
diff --git a/Lib/fontTools/pens/t2CharStringPen.py b/Lib/fontTools/pens/t2CharStringPen.py
index 8cc5e088..2025fd55 100644
--- a/Lib/fontTools/pens/t2CharStringPen.py
+++ b/Lib/fontTools/pens/t2CharStringPen.py
@@ -9,26 +9,26 @@ from fontTools.pens.basePen import BasePen
from fontTools.cffLib.specializer import specializeCommands, commandsToProgram
+def t2c_round(number, tolerance=0.5):
+ if tolerance == 0:
+ return number # no-op
+ rounded = otRound(number)
+ # return rounded integer if the tolerance >= 0.5, or if the absolute
+ # difference between the original float and the rounded integer is
+ # within the tolerance
+ if tolerance >= .5 or abs(rounded - number) <= tolerance:
+ return rounded
+ else:
+ # else return the value un-rounded
+ return number
+
def makeRoundFunc(tolerance):
if tolerance < 0:
raise ValueError("Rounding tolerance must be positive")
- def _round(number):
- if tolerance == 0:
- return number # no-op
- rounded = otRound(number)
- # return rounded integer if the tolerance >= 0.5, or if the absolute
- # difference between the original float and the rounded integer is
- # within the tolerance
- if tolerance >= .5 or abs(rounded - number) <= tolerance:
- return rounded
- else:
- # else return the value un-rounded
- return number
-
def roundPoint(point):
x, y = point
- return _round(x), _round(y)
+ return t2c_round(x, tolerance), t2c_round(y, tolerance)
return roundPoint
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index 6ed54e5f..2ed17cc9 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -7,9 +7,9 @@ from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import otRound
from fontTools import ttLib
from fontTools.ttLib.tables import otTables
-from fontTools.misc import psCharStrings
from fontTools.pens.basePen import NullPen
from fontTools.misc.loggingTools import Timer
+from fontTools.subset.cff import *
from fontTools.varLib import varStore
import sys
import struct
@@ -378,12 +378,6 @@ def _add_method(*clazzes):
def _uniq_sort(l):
return sorted(set(l))
-def _set_update(s, *others):
- # Jython's set.update only takes one other argument.
- # Emulate real set.update...
- for other in others:
- s.update(other)
-
def _dict_subset(d, glyphs):
return {g:d[g] for g in glyphs}
@@ -457,7 +451,7 @@ def subset_glyphs(self, s):
def closure_glyphs(self, s, cur_glyphs):
for glyph, subst in self.mapping.items():
if glyph in cur_glyphs:
- _set_update(s.glyphs, subst)
+ s.glyphs.update(subst)
@_add_method(otTables.MultipleSubst)
def subset_glyphs(self, s):
@@ -467,8 +461,8 @@ def subset_glyphs(self, s):
@_add_method(otTables.AlternateSubst)
def closure_glyphs(self, s, cur_glyphs):
- _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items()
- if g in cur_glyphs))
+ s.glyphs.update(*(vlist for g,vlist in self.alternates.items()
+ if g in cur_glyphs))
@_add_method(otTables.AlternateSubst)
def subset_glyphs(self, s):
@@ -480,10 +474,10 @@ def subset_glyphs(self, s):
@_add_method(otTables.LigatureSubst)
def closure_glyphs(self, s, cur_glyphs):
- _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs
- if all(c in s.glyphs for c in seq.Component)]
- for g,seqs in self.ligatures.items()
- if g in cur_glyphs))
+ s.glyphs.update(*([seq.LigGlyph for seq in seqs
+ if all(c in s.glyphs for c in seq.Component)]
+ for g,seqs in self.ligatures.items()
+ if g in cur_glyphs))
@_add_method(otTables.LigatureSubst)
def subset_glyphs(self, s):
@@ -2087,517 +2081,6 @@ def prune_post_subset(self, font, options):
return True
-class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler):
-
- def __init__(self, components, localSubrs, globalSubrs):
- psCharStrings.SimpleT2Decompiler.__init__(self,
- localSubrs,
- globalSubrs)
- self.components = components
-
- def op_endchar(self, index):
- args = self.popall()
- if len(args) >= 4:
- from fontTools.encodings.StandardEncoding import StandardEncoding
- # endchar can do seac accent bulding; The T2 spec says it's deprecated,
- # but recent software that shall remain nameless does output it.
- adx, ady, bchar, achar = args[-4:]
- baseGlyph = StandardEncoding[bchar]
- accentGlyph = StandardEncoding[achar]
- self.components.add(baseGlyph)
- self.components.add(accentGlyph)
-
-@_add_method(ttLib.getTableClass('CFF '))
-def closure_glyphs(self, s):
- cff = self.cff
- assert len(cff) == 1
- font = cff[cff.keys()[0]]
- glyphSet = font.CharStrings
-
- decompose = s.glyphs
- while decompose:
- components = set()
- for g in decompose:
- if g not in glyphSet:
- continue
- gl = glyphSet[g]
-
- subrs = getattr(gl.private, "Subrs", [])
- decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs)
- decompiler.execute(gl)
- components -= s.glyphs
- s.glyphs.update(components)
- decompose = components
-
-@_add_method(ttLib.getTableClass('CFF '))
-def prune_pre_subset(self, font, options):
- cff = self.cff
- # CFF table must have one font only
- cff.fontNames = cff.fontNames[:1]
-
- if options.notdef_glyph and not options.notdef_outline:
- 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']
-
- # Clear useless Encoding
- for fontname in cff.keys():
- font = cff[fontname]
- # https://github.com/behdad/fonttools/issues/620
- font.Encoding = "StandardEncoding"
-
- return True # bool(cff.fontNames)
-
-@_add_method(ttLib.getTableClass('CFF '))
-def subset_glyphs(self, s):
- cff = self.cff
- for fontname in cff.keys():
- 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}
- 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())
-
-@_add_method(psCharStrings.T2CharString)
-def subset_subroutines(self, subrs, gsubrs):
- p = self.program
- assert len(p)
- for i in range(1, len(p)):
- if p[i] == 'callsubr':
- assert isinstance(p[i-1], int)
- p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias
- elif p[i] == 'callgsubr':
- assert isinstance(p[i-1], int)
- p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias
-
-@_add_method(psCharStrings.T2CharString)
-def drop_hints(self):
- hints = self._hints
-
- if hints.deletions:
- p = self.program
- for idx in reversed(hints.deletions):
- del p[idx-2:idx]
-
- if hints.has_hint:
- assert not hints.deletions or hints.last_hint <= hints.deletions[0]
- self.program = self.program[hints.last_hint:]
- if hasattr(self, 'width'):
- # Insert width back if needed
- if self.width != self.private.defaultWidthX:
- self.program.insert(0, self.width - self.private.nominalWidthX)
-
- if hints.has_hintmask:
- i = 0
- p = self.program
- while i < len(p):
- if p[i] in ['hintmask', 'cntrmask']:
- assert i + 1 <= len(p)
- del p[i:i+2]
- continue
- i += 1
-
- assert len(self.program)
-
- del self._hints
-
-class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):
-
- def __init__(self, localSubrs, globalSubrs):
- psCharStrings.SimpleT2Decompiler.__init__(self,
- localSubrs,
- globalSubrs)
- for subrs in [localSubrs, globalSubrs]:
- if subrs and not hasattr(subrs, "_used"):
- subrs._used = set()
-
- def op_callsubr(self, index):
- self.localSubrs._used.add(self.operandStack[-1]+self.localBias)
- psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
-
- def op_callgsubr(self, index):
- self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias)
- psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
-
-class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor):
-
- class Hints(object):
- def __init__(self):
- # Whether calling this charstring produces any hint stems
- # Note that if a charstring starts with hintmask, it will
- # have has_hint set to True, because it *might* produce an
- # implicit vstem if called under certain conditions.
- self.has_hint = False
- # Index to start at to drop all hints
- self.last_hint = 0
- # Index up to which we know more hints are possible.
- # Only relevant if status is 0 or 1.
- self.last_checked = 0
- # The status means:
- # 0: after dropping hints, this charstring is empty
- # 1: after dropping hints, there may be more hints
- # continuing after this
- # 2: no more hints possible after this charstring
- self.status = 0
- # Has hintmask instructions; not recursive
- self.has_hintmask = False
- # List of indices of calls to empty subroutines to remove.
- self.deletions = []
- pass
-
- def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX):
- self._css = css
- psCharStrings.T2WidthExtractor.__init__(
- self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX)
-
- def execute(self, charString):
- old_hints = charString._hints if hasattr(charString, '_hints') else None
- charString._hints = self.Hints()
-
- psCharStrings.T2WidthExtractor.execute(self, charString)
-
- hints = charString._hints
-
- if hints.has_hint or hints.has_hintmask:
- self._css.add(charString)
-
- if hints.status != 2:
- # Check from last_check, make sure we didn't have any operators.
- for i in range(hints.last_checked, len(charString.program) - 1):
- if isinstance(charString.program[i], str):
- hints.status = 2
- break
- else:
- hints.status = 1 # There's *something* here
- hints.last_checked = len(charString.program)
-
- if old_hints:
- assert hints.__dict__ == old_hints.__dict__
-
- def op_callsubr(self, index):
- subr = self.localSubrs[self.operandStack[-1]+self.localBias]
- psCharStrings.T2WidthExtractor.op_callsubr(self, index)
- self.processSubr(index, subr)
-
- def op_callgsubr(self, index):
- subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
- psCharStrings.T2WidthExtractor.op_callgsubr(self, index)
- self.processSubr(index, subr)
-
- def op_hstem(self, index):
- psCharStrings.T2WidthExtractor.op_hstem(self, index)
- self.processHint(index)
- def op_vstem(self, index):
- psCharStrings.T2WidthExtractor.op_vstem(self, index)
- self.processHint(index)
- def op_hstemhm(self, index):
- psCharStrings.T2WidthExtractor.op_hstemhm(self, index)
- self.processHint(index)
- def op_vstemhm(self, index):
- psCharStrings.T2WidthExtractor.op_vstemhm(self, index)
- self.processHint(index)
- def op_hintmask(self, index):
- rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index)
- self.processHintmask(index)
- return rv
- def op_cntrmask(self, index):
- rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index)
- self.processHintmask(index)
- return rv
-
- def processHintmask(self, index):
- cs = self.callingStack[-1]
- hints = cs._hints
- hints.has_hintmask = True
- if hints.status != 2:
- # Check from last_check, see if we may be an implicit vstem
- for i in range(hints.last_checked, index - 1):
- if isinstance(cs.program[i], str):
- hints.status = 2
- break
- else:
- # We are an implicit vstem
- hints.has_hint = True
- hints.last_hint = index + 1
- hints.status = 0
- hints.last_checked = index + 1
-
- def processHint(self, index):
- cs = self.callingStack[-1]
- hints = cs._hints
- hints.has_hint = True
- hints.last_hint = index
- hints.last_checked = index
-
- def processSubr(self, index, subr):
- cs = self.callingStack[-1]
- hints = cs._hints
- subr_hints = subr._hints
-
- # Check from last_check, make sure we didn't have
- # any operators.
- if hints.status != 2:
- for i in range(hints.last_checked, index - 1):
- if isinstance(cs.program[i], str):
- hints.status = 2
- break
- hints.last_checked = index
-
- if hints.status != 2:
- if subr_hints.has_hint:
- hints.has_hint = True
-
- # Decide where to chop off from
- if subr_hints.status == 0:
- hints.last_hint = index
- else:
- hints.last_hint = index - 2 # Leave the subr call in
- elif subr_hints.status == 0:
- hints.deletions.append(index)
-
- hints.status = max(hints.status, subr_hints.status)
-
-class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
-
- def __init__(self, localSubrs, globalSubrs):
- psCharStrings.SimpleT2Decompiler.__init__(self,
- localSubrs,
- globalSubrs)
-
- def execute(self, charString):
- # Note: Currently we recompute _desubroutinized each time.
- # This is more robust in some cases, but in other places we assume
- # that each subroutine always expands to the same code, so
- # maybe it doesn't matter. To speed up we can just not
- # recompute _desubroutinized if it's there. For now I just
- # double-check that it desubroutinized to the same thing.
- old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None
-
- charString._patches = []
- psCharStrings.SimpleT2Decompiler.execute(self, charString)
- desubroutinized = charString.program[:]
- for idx,expansion in reversed (charString._patches):
- assert idx >= 2
- assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1]
- assert type(desubroutinized[idx - 2]) == int
- if expansion[-1] == 'return':
- expansion = expansion[:-1]
- desubroutinized[idx-2:idx] = expansion
- if 'endchar' in desubroutinized:
- # Cut off after first endchar
- desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
- else:
- if not len(desubroutinized) or desubroutinized[-1] != 'return':
- desubroutinized.append('return')
-
- charString._desubroutinized = desubroutinized
- del charString._patches
-
- if old_desubroutinized:
- assert desubroutinized == old_desubroutinized
-
- def op_callsubr(self, index):
- subr = self.localSubrs[self.operandStack[-1]+self.localBias]
- psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
- self.processSubr(index, subr)
-
- def op_callgsubr(self, index):
- subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
- psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
- self.processSubr(index, subr)
-
- def processSubr(self, index, subr):
- cs = self.callingStack[-1]
- cs._patches.append((index, subr._desubroutinized))
-
-
-@_add_method(ttLib.getTableClass('CFF '))
-def prune_post_subset(self, font, options):
- cff = self.cff
- for fontname in cff.keys():
- font = cff[fontname]
- cs = font.CharStrings
-
- # Drop unused FontDictionaries
- if hasattr(font, "FDSelect"):
- sel = font.FDSelect
- indices = _uniq_sort(sel.gidArray)
- sel.gidArray = [indices.index (ss) for ss in sel.gidArray]
- arr = font.FDArray
- arr.items = [arr[i] for i in indices]
- del arr.file, arr.offsets
-
- # Desubroutinize if asked for
- if options.desubroutinize:
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
- c.decompile()
- subrs = getattr(c.private, "Subrs", [])
- decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs)
- decompiler.execute(c)
- c.program = c._desubroutinized
-
- # Drop hints if not needed
- if not options.hinting:
-
- # This can be tricky, but doesn't have to. What we do is:
- #
- # - Run all used glyph charstrings and recurse into subroutines,
- # - For each charstring (including subroutines), if it has any
- # of the hint stem operators, we mark it as such.
- # Upon returning, for each charstring we note all the
- # subroutine calls it makes that (recursively) contain a stem,
- # - Dropping hinting then consists of the following two ops:
- # * Drop the piece of the program in each charstring before the
- # last call to a stem op or a stem-calling subroutine,
- # * Drop all hintmask operations.
- # - It's trickier... A hintmask right after hints and a few numbers
- # will act as an implicit vstemhm. As such, we track whether
- # we have seen any non-hint operators so far and do the right
- # thing, recursively... Good luck understanding that :(
- css = set()
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
- c.decompile()
- subrs = getattr(c.private, "Subrs", [])
- decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs,
- c.private.nominalWidthX,
- c.private.defaultWidthX)
- decompiler.execute(c)
- c.width = decompiler.width
- for charstring in css:
- charstring.drop_hints()
- del css
-
- # Drop font-wide hinting values
- all_privs = []
- if hasattr(font, 'FDSelect'):
- all_privs.extend(fd.Private for fd in font.FDArray)
- else:
- all_privs.append(font.Private)
- for priv in all_privs:
- for k in ['BlueValues', 'OtherBlues',
- 'FamilyBlues', 'FamilyOtherBlues',
- 'BlueScale', 'BlueShift', 'BlueFuzz',
- 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW',
- 'ForceBold', 'LanguageGroup', 'ExpansionFactor']:
- if hasattr(priv, k):
- setattr(priv, k, None)
-
- # Renumber subroutines to remove unused ones
-
- # Mark all used subroutines
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
- subrs = getattr(c.private, "Subrs", [])
- decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs)
- decompiler.execute(c)
-
- all_subrs = [font.GlobalSubrs]
- if hasattr(font, 'FDSelect'):
- all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs)
- elif hasattr(font.Private, 'Subrs') and font.Private.Subrs:
- all_subrs.append(font.Private.Subrs)
-
- subrs = set(subrs) # Remove duplicates
-
- # Prepare
- for subrs in all_subrs:
- if not hasattr(subrs, '_used'):
- subrs._used = set()
- subrs._used = _uniq_sort(subrs._used)
- subrs._old_bias = psCharStrings.calcSubrBias(subrs)
- subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)
-
- # Renumber glyph charstrings
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
- subrs = getattr(c.private, "Subrs", [])
- c.subset_subroutines (subrs, font.GlobalSubrs)
-
- # Renumber subroutines themselves
- for subrs in all_subrs:
- if subrs == font.GlobalSubrs:
- if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'):
- local_subrs = font.Private.Subrs
- else:
- local_subrs = []
- else:
- local_subrs = subrs
-
- subrs.items = [subrs.items[i] for i in subrs._used]
- if hasattr(subrs, 'file'):
- del subrs.file
- if hasattr(subrs, 'offsets'):
- del subrs.offsets
-
- for subr in subrs.items:
- subr.subset_subroutines (local_subrs, font.GlobalSubrs)
-
- # Delete local SubrsIndex if empty
- if hasattr(font, 'FDSelect'):
- for fd in font.FDArray:
- _delete_empty_subrs(fd.Private)
- else:
- _delete_empty_subrs(font.Private)
-
- # Cleanup
- for subrs in all_subrs:
- del subrs._used, subrs._old_bias, subrs._new_bias
-
- return True
-
-
-def _delete_empty_subrs(private_dict):
- if hasattr(private_dict, 'Subrs') and not private_dict.Subrs:
- if 'Subrs' in private_dict.rawDict:
- del private_dict.rawDict['Subrs']
- del private_dict.Subrs
-
-
@_add_method(ttLib.getTableClass('cmap'))
def closure_glyphs(self, s):
tables = [t for t in self.tables if t.isUnicode()]
@@ -2691,7 +2174,8 @@ def prune_pre_subset(self, font, options):
if inst.postscriptNameID != 0xFFFF])
stat = font.get('STAT')
if stat:
- nameIDs.update([val_rec.ValueNameID for val_rec in stat.table.AxisValueArray.AxisValue])
+ if stat.table.AxisValueArray:
+ nameIDs.update([val_rec.ValueNameID for val_rec in stat.table.AxisValueArray.AxisValue])
nameIDs.update([axis_rec.AxisNameID for axis_rec in stat.table.DesignAxisRecord.Axis])
if '*' not in options.name_IDs:
self.names = [n for n in self.names if n.nameID in nameIDs]
@@ -3257,7 +2741,8 @@ def main(args=None):
text += g[7:]
continue
if g.startswith('--text-file='):
- text += open(g[12:], encoding='utf-8').read().replace('\n', '')
+ with open(g[12:], encoding='utf-8') as f:
+ text += f.read().replace('\n', '')
continue
if g.startswith('--unicodes='):
if g[11:] == '*':
@@ -3266,15 +2751,17 @@ def main(args=None):
unicodes.extend(parse_unicodes(g[11:]))
continue
if g.startswith('--unicodes-file='):
- for line in open(g[16:]).readlines():
- unicodes.extend(parse_unicodes(line.split('#')[0]))
+ with open(g[16:]) as f:
+ for line in f.readlines():
+ unicodes.extend(parse_unicodes(line.split('#')[0]))
continue
if g.startswith('--gids='):
gids.extend(parse_gids(g[7:]))
continue
if g.startswith('--gids-file='):
- for line in open(g[12:]).readlines():
- gids.extend(parse_gids(line.split('#')[0]))
+ with open(g[12:]) as f:
+ for line in f.readlines():
+ gids.extend(parse_gids(line.split('#')[0]))
continue
if g.startswith('--glyphs='):
if g[9:] == '*':
@@ -3283,8 +2770,9 @@ def main(args=None):
glyphs.extend(parse_glyphs(g[9:]))
continue
if g.startswith('--glyphs-file='):
- for line in open(g[14:]).readlines():
- glyphs.extend(parse_glyphs(line.split('#')[0]))
+ with open(g[14:]) as f:
+ for line in f.readlines():
+ glyphs.extend(parse_glyphs(line.split('#')[0]))
continue
glyphs.append(g)
diff --git a/Lib/fontTools/subset/cff.py b/Lib/fontTools/subset/cff.py
new file mode 100644
index 00000000..9a2b77e4
--- /dev/null
+++ b/Lib/fontTools/subset/cff.py
@@ -0,0 +1,579 @@
+from fontTools.misc import psCharStrings
+from fontTools import ttLib
+from fontTools.pens.basePen import NullPen
+from fontTools.misc.fixedTools import otRound
+from fontTools.varLib.varStore import VarStoreInstancer
+
+def _add_method(*clazzes):
+ """Returns a decorator function that adds a new method to one or
+ more classes."""
+ def wrapper(method):
+ done = []
+ for clazz in clazzes:
+ if clazz in done: continue # Support multiple names of a clazz
+ done.append(clazz)
+ assert clazz.__name__ != 'DefaultTable', \
+ 'Oops, table class not found.'
+ assert not hasattr(clazz, method.__name__), \
+ "Oops, class '%s' has method '%s'." % (clazz.__name__,
+ method.__name__)
+ setattr(clazz, method.__name__, method)
+ return None
+ return wrapper
+
+def _uniq_sort(l):
+ return sorted(set(l))
+
+class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler):
+
+ def __init__(self, components, localSubrs, globalSubrs):
+ psCharStrings.SimpleT2Decompiler.__init__(self,
+ localSubrs,
+ globalSubrs)
+ self.components = components
+
+ def op_endchar(self, index):
+ args = self.popall()
+ if len(args) >= 4:
+ from fontTools.encodings.StandardEncoding import StandardEncoding
+ # endchar can do seac accent bulding; The T2 spec says it's deprecated,
+ # but recent software that shall remain nameless does output it.
+ adx, ady, bchar, achar = args[-4:]
+ baseGlyph = StandardEncoding[bchar]
+ accentGlyph = StandardEncoding[achar]
+ self.components.add(baseGlyph)
+ self.components.add(accentGlyph)
+
+@_add_method(ttLib.getTableClass('CFF '))
+def closure_glyphs(self, s):
+ cff = self.cff
+ assert len(cff) == 1
+ font = cff[cff.keys()[0]]
+ glyphSet = font.CharStrings
+
+ decompose = s.glyphs
+ while decompose:
+ components = set()
+ for g in decompose:
+ if g not in glyphSet:
+ continue
+ gl = glyphSet[g]
+
+ subrs = getattr(gl.private, "Subrs", [])
+ decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs)
+ decompiler.execute(gl)
+ components -= s.glyphs
+ s.glyphs.update(components)
+ decompose = components
+
+@_add_method(ttLib.getTableClass('CFF '))
+def prune_pre_subset(self, font, options):
+ cff = self.cff
+ # CFF table must have one font only
+ cff.fontNames = cff.fontNames[:1]
+
+ if options.notdef_glyph and not options.notdef_outline:
+ 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']
+
+ # Clear useless Encoding
+ for fontname in cff.keys():
+ font = cff[fontname]
+ # https://github.com/behdad/fonttools/issues/620
+ font.Encoding = "StandardEncoding"
+
+ return True # bool(cff.fontNames)
+
+@_add_method(ttLib.getTableClass('CFF '))
+def subset_glyphs(self, s):
+ cff = self.cff
+ for fontname in cff.keys():
+ 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}
+ 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())
+
+@_add_method(psCharStrings.T2CharString)
+def subset_subroutines(self, subrs, gsubrs):
+ p = self.program
+ for i in range(1, len(p)):
+ if p[i] == 'callsubr':
+ assert isinstance(p[i-1], int)
+ p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias
+ elif p[i] == 'callgsubr':
+ assert isinstance(p[i-1], int)
+ p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias
+
+@_add_method(psCharStrings.T2CharString)
+def drop_hints(self):
+ hints = self._hints
+
+ if hints.deletions:
+ p = self.program
+ for idx in reversed(hints.deletions):
+ del p[idx-2:idx]
+
+ if hints.has_hint:
+ assert not hints.deletions or hints.last_hint <= hints.deletions[0]
+ self.program = self.program[hints.last_hint:]
+ if not self.program:
+ # TODO CFF2 no need for endchar.
+ self.program.append('endchar')
+ if hasattr(self, 'width'):
+ # Insert width back if needed
+ if self.width != self.private.defaultWidthX:
+ # For CFF2 charstrings, this should never happen
+ assert self.private.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value"
+ self.program.insert(0, self.width - self.private.nominalWidthX)
+
+ if hints.has_hintmask:
+ i = 0
+ p = self.program
+ while i < len(p):
+ if p[i] in ['hintmask', 'cntrmask']:
+ assert i + 1 <= len(p)
+ del p[i:i+2]
+ continue
+ i += 1
+
+ assert len(self.program)
+
+ del self._hints
+
+class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):
+
+ def __init__(self, localSubrs, globalSubrs, private):
+ psCharStrings.SimpleT2Decompiler.__init__(self,
+ localSubrs,
+ globalSubrs,
+ private)
+ for subrs in [localSubrs, globalSubrs]:
+ if subrs and not hasattr(subrs, "_used"):
+ subrs._used = set()
+
+ def op_callsubr(self, index):
+ self.localSubrs._used.add(self.operandStack[-1]+self.localBias)
+ psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
+
+ def op_callgsubr(self, index):
+ self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias)
+ psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
+
+class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor):
+
+ class Hints(object):
+ def __init__(self):
+ # Whether calling this charstring produces any hint stems
+ # Note that if a charstring starts with hintmask, it will
+ # have has_hint set to True, because it *might* produce an
+ # implicit vstem if called under certain conditions.
+ self.has_hint = False
+ # Index to start at to drop all hints
+ self.last_hint = 0
+ # Index up to which we know more hints are possible.
+ # Only relevant if status is 0 or 1.
+ self.last_checked = 0
+ # The status means:
+ # 0: after dropping hints, this charstring is empty
+ # 1: after dropping hints, there may be more hints
+ # continuing after this, or there might be
+ # other things. Not clear yet.
+ # 2: no more hints possible after this charstring
+ self.status = 0
+ # Has hintmask instructions; not recursive
+ self.has_hintmask = False
+ # List of indices of calls to empty subroutines to remove.
+ self.deletions = []
+ pass
+
+ def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None):
+ self._css = css
+ psCharStrings.T2WidthExtractor.__init__(
+ self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX)
+ self.private = private
+
+ def execute(self, charString):
+ old_hints = charString._hints if hasattr(charString, '_hints') else None
+ charString._hints = self.Hints()
+
+ psCharStrings.T2WidthExtractor.execute(self, charString)
+
+ hints = charString._hints
+
+ if hints.has_hint or hints.has_hintmask:
+ self._css.add(charString)
+
+ if hints.status != 2:
+ # Check from last_check, make sure we didn't have any operators.
+ for i in range(hints.last_checked, len(charString.program) - 1):
+ if isinstance(charString.program[i], str):
+ hints.status = 2
+ break
+ else:
+ hints.status = 1 # There's *something* here
+ hints.last_checked = len(charString.program)
+
+ if old_hints:
+ assert hints.__dict__ == old_hints.__dict__
+
+ def op_callsubr(self, index):
+ subr = self.localSubrs[self.operandStack[-1]+self.localBias]
+ psCharStrings.T2WidthExtractor.op_callsubr(self, index)
+ self.processSubr(index, subr)
+
+ def op_callgsubr(self, index):
+ subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
+ psCharStrings.T2WidthExtractor.op_callgsubr(self, index)
+ self.processSubr(index, subr)
+
+ def op_hstem(self, index):
+ psCharStrings.T2WidthExtractor.op_hstem(self, index)
+ self.processHint(index)
+ def op_vstem(self, index):
+ psCharStrings.T2WidthExtractor.op_vstem(self, index)
+ self.processHint(index)
+ def op_hstemhm(self, index):
+ psCharStrings.T2WidthExtractor.op_hstemhm(self, index)
+ self.processHint(index)
+ def op_vstemhm(self, index):
+ psCharStrings.T2WidthExtractor.op_vstemhm(self, index)
+ self.processHint(index)
+ def op_hintmask(self, index):
+ rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index)
+ self.processHintmask(index)
+ return rv
+ def op_cntrmask(self, index):
+ rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index)
+ self.processHintmask(index)
+ return rv
+
+ def processHintmask(self, index):
+ cs = self.callingStack[-1]
+ hints = cs._hints
+ hints.has_hintmask = True
+ if hints.status != 2:
+ # Check from last_check, see if we may be an implicit vstem
+ for i in range(hints.last_checked, index - 1):
+ if isinstance(cs.program[i], str):
+ hints.status = 2
+ break
+ else:
+ # We are an implicit vstem
+ hints.has_hint = True
+ hints.last_hint = index + 1
+ hints.status = 0
+ hints.last_checked = index + 1
+
+ def processHint(self, index):
+ cs = self.callingStack[-1]
+ hints = cs._hints
+ hints.has_hint = True
+ hints.last_hint = index
+ hints.last_checked = index
+
+ def processSubr(self, index, subr):
+ cs = self.callingStack[-1]
+ hints = cs._hints
+ subr_hints = subr._hints
+
+ # Check from last_check, make sure we didn't have
+ # any operators.
+ if hints.status != 2:
+ for i in range(hints.last_checked, index - 1):
+ if isinstance(cs.program[i], str):
+ hints.status = 2
+ break
+ hints.last_checked = index
+
+ if hints.status != 2:
+ if subr_hints.has_hint:
+ hints.has_hint = True
+
+ # Decide where to chop off from
+ if subr_hints.status == 0:
+ hints.last_hint = index
+ else:
+ hints.last_hint = index - 2 # Leave the subr call in
+
+ elif subr_hints.status == 0:
+ hints.deletions.append(index)
+
+ hints.status = max(hints.status, subr_hints.status)
+
+class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
+
+ def __init__(self, localSubrs, globalSubrs, private=None):
+ psCharStrings.SimpleT2Decompiler.__init__(self,
+ localSubrs,
+ globalSubrs, private)
+
+ def execute(self, charString):
+ if hasattr(charString, '_desubroutinized'):
+ return
+
+ charString._patches = []
+ psCharStrings.SimpleT2Decompiler.execute(self, charString)
+ desubroutinized = charString.program[:]
+ for idx,expansion in reversed (charString._patches):
+ assert idx >= 2
+ assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1]
+ assert type(desubroutinized[idx - 2]) == int
+ if expansion[-1] == 'return':
+ expansion = expansion[:-1]
+ desubroutinized[idx-2:idx] = expansion
+ if not self.private.in_cff2:
+ if 'endchar' in desubroutinized:
+ # Cut off after first endchar
+ desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
+ else:
+ if not len(desubroutinized) or desubroutinized[-1] != 'return':
+ desubroutinized.append('return')
+
+ charString._desubroutinized = desubroutinized
+ del charString._patches
+
+ def op_callsubr(self, index):
+ subr = self.localSubrs[self.operandStack[-1]+self.localBias]
+ psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
+ self.processSubr(index, subr)
+
+ def op_callgsubr(self, index):
+ subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
+ psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
+ self.processSubr(index, subr)
+
+ def processSubr(self, index, subr):
+ cs = self.callingStack[-1]
+ cs._patches.append((index, subr._desubroutinized))
+
+
+@_add_method(ttLib.getTableClass('CFF '))
+def prune_post_subset(self, ttfFont, options):
+ cff = self.cff
+ for fontname in cff.keys():
+ font = cff[fontname]
+ cs = font.CharStrings
+
+ # Drop unused FontDictionaries
+ if hasattr(font, "FDSelect"):
+ sel = font.FDSelect
+ indices = _uniq_sort(sel.gidArray)
+ sel.gidArray = [indices.index (ss) for ss in sel.gidArray]
+ arr = font.FDArray
+ arr.items = [arr[i] for i in indices]
+ del arr.file, arr.offsets
+
+ # Desubroutinize if asked for
+ if options.desubroutinize:
+ self.desubroutinize()
+ else:
+ for fontname in cff.keys():
+ font = cff[fontname]
+ self.remove_unused_subroutines()
+
+ # Drop hints if not needed
+ if not options.hinting:
+ self.remove_hints()
+
+
+ return True
+
+
+def _delete_empty_subrs(private_dict):
+ if hasattr(private_dict, 'Subrs') and not private_dict.Subrs:
+ if 'Subrs' in private_dict.rawDict:
+ del private_dict.rawDict['Subrs']
+ del private_dict.Subrs
+
+@_add_method(ttLib.getTableClass('CFF '))
+def desubroutinize(self):
+ cff = self.cff
+ for fontname in cff.keys():
+ font = cff[fontname]
+ cs = font.CharStrings
+ for g in font.charset:
+ c, _ = cs.getItemAndSelector(g)
+ c.decompile()
+ subrs = getattr(c.private, "Subrs", [])
+ decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private)
+ decompiler.execute(c)
+ c.program = c._desubroutinized
+ del c._desubroutinized
+ # Delete All the Subrs!!!
+ if font.GlobalSubrs:
+ del font.GlobalSubrs
+ if hasattr(font, 'FDArray'):
+ for fd in font.FDArray:
+ pd = fd.Private
+ if hasattr(pd, 'Subrs'):
+ del pd.Subrs
+ if 'Subrs' in pd.rawDict:
+ del pd.rawDict['Subrs']
+ self.remove_unused_subroutines()
+
+
+@_add_method(ttLib.getTableClass('CFF '))
+def remove_hints(self):
+ cff = self.cff
+ for fontname in cff.keys():
+ font = cff[fontname]
+ cs = font.CharStrings
+ # This can be tricky, but doesn't have to. What we do is:
+ #
+ # - Run all used glyph charstrings and recurse into subroutines,
+ # - For each charstring (including subroutines), if it has any
+ # of the hint stem operators, we mark it as such.
+ # Upon returning, for each charstring we note all the
+ # subroutine calls it makes that (recursively) contain a stem,
+ # - Dropping hinting then consists of the following two ops:
+ # * Drop the piece of the program in each charstring before the
+ # last call to a stem op or a stem-calling subroutine,
+ # * Drop all hintmask operations.
+ # - It's trickier... A hintmask right after hints and a few numbers
+ # will act as an implicit vstemhm. As such, we track whether
+ # we have seen any non-hint operators so far and do the right
+ # thing, recursively... Good luck understanding that :(
+ css = set()
+ for g in font.charset:
+ c, _ = cs.getItemAndSelector(g)
+ c.decompile()
+ subrs = getattr(c.private, "Subrs", [])
+ decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs,
+ c.private.nominalWidthX,
+ c.private.defaultWidthX,
+ c.private)
+ decompiler.execute(c)
+ c.width = decompiler.width
+ for charstring in css:
+ charstring.drop_hints()
+ del css
+
+ # Drop font-wide hinting values
+ all_privs = []
+ if hasattr(font, 'FDArray'):
+ all_privs.extend(fd.Private for fd in font.FDArray)
+ else:
+ all_privs.append(font.Private)
+ for priv in all_privs:
+ for k in ['BlueValues', 'OtherBlues',
+ 'FamilyBlues', 'FamilyOtherBlues',
+ 'BlueScale', 'BlueShift', 'BlueFuzz',
+ 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW',
+ 'ForceBold', 'LanguageGroup', 'ExpansionFactor']:
+ if hasattr(priv, k):
+ setattr(priv, k, None)
+ self.remove_unused_subroutines()
+
+
+@_add_method(ttLib.getTableClass('CFF '))
+def remove_unused_subroutines(self):
+ cff = self.cff
+ for fontname in cff.keys():
+ font = cff[fontname]
+ cs = font.CharStrings
+ # Renumber subroutines to remove unused ones
+
+ # Mark all used subroutines
+ for g in font.charset:
+ c, _ = cs.getItemAndSelector(g)
+ subrs = getattr(c.private, "Subrs", [])
+ decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
+ decompiler.execute(c)
+
+ all_subrs = [font.GlobalSubrs]
+ if hasattr(font, 'FDArray'):
+ all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs)
+ elif hasattr(font.Private, 'Subrs') and font.Private.Subrs:
+ all_subrs.append(font.Private.Subrs)
+
+ subrs = set(subrs) # Remove duplicates
+
+ # Prepare
+ for subrs in all_subrs:
+ if not hasattr(subrs, '_used'):
+ subrs._used = set()
+ subrs._used = _uniq_sort(subrs._used)
+ subrs._old_bias = psCharStrings.calcSubrBias(subrs)
+ subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)
+
+ # Renumber glyph charstrings
+ for g in font.charset:
+ c, _ = cs.getItemAndSelector(g)
+ subrs = getattr(c.private, "Subrs", [])
+ c.subset_subroutines (subrs, font.GlobalSubrs)
+
+ # Renumber subroutines themselves
+ for subrs in all_subrs:
+ if subrs == font.GlobalSubrs:
+ if not hasattr(font, 'FDArray') and hasattr(font.Private, 'Subrs'):
+ local_subrs = font.Private.Subrs
+ else:
+ local_subrs = []
+ else:
+ local_subrs = subrs
+
+ subrs.items = [subrs.items[i] for i in subrs._used]
+ if hasattr(subrs, 'file'):
+ del subrs.file
+ if hasattr(subrs, 'offsets'):
+ del subrs.offsets
+
+ for subr in subrs.items:
+ subr.subset_subroutines (local_subrs, font.GlobalSubrs)
+
+ # Delete local SubrsIndex if empty
+ if hasattr(font, 'FDArray'):
+ for fd in font.FDArray:
+ _delete_empty_subrs(fd.Private)
+ else:
+ _delete_empty_subrs(font.Private)
+
+ # Cleanup
+ for subrs in all_subrs:
+ del subrs._used, subrs._old_bias, subrs._new_bias
+
diff --git a/Lib/fontTools/t1Lib/__init__.py b/Lib/fontTools/t1Lib/__init__.py
index 5da9cea4..db5189a5 100644
--- a/Lib/fontTools/t1Lib/__init__.py
+++ b/Lib/fontTools/t1Lib/__init__.py
@@ -108,11 +108,12 @@ class T1Font(object):
def read(path, onlyHeader=False):
"""reads any Type 1 font file, returns raw data"""
- normpath = path.lower()
+ _, ext = os.path.splitext(path)
+ ext = ext.lower()
creator, typ = getMacCreatorAndType(path)
if typ == 'LWFN':
return readLWFN(path, onlyHeader), 'LWFN'
- if normpath[-4:] == '.pfb':
+ if ext == '.pfb':
return readPFB(path, onlyHeader), 'PFB'
else:
return readOther(path), 'OTHER'
@@ -164,9 +165,8 @@ def readLWFN(path, onlyHeader=False):
elif code in [3, 5]:
break
elif code == 4:
- f = open(path, "rb")
- data.append(f.read())
- f.close()
+ with open(path, "rb") as f:
+ data.append(f.read())
elif code == 0:
pass # comment, ignore
else:
@@ -179,35 +179,32 @@ def readLWFN(path, onlyHeader=False):
def readPFB(path, onlyHeader=False):
"""reads a PFB font file, returns raw data"""
- f = open(path, "rb")
data = []
- while True:
- if f.read(1) != bytechr(128):
- raise T1Error('corrupt PFB file')
- code = byteord(f.read(1))
- if code in [1, 2]:
- chunklen = stringToLong(f.read(4))
- chunk = f.read(chunklen)
- assert len(chunk) == chunklen
- data.append(chunk)
- elif code == 3:
- break
- else:
- raise T1Error('bad chunk code: ' + repr(code))
- if onlyHeader:
- break
- f.close()
+ with open(path, "rb") as f:
+ while True:
+ if f.read(1) != bytechr(128):
+ raise T1Error('corrupt PFB file')
+ code = byteord(f.read(1))
+ if code in [1, 2]:
+ chunklen = stringToLong(f.read(4))
+ chunk = f.read(chunklen)
+ assert len(chunk) == chunklen
+ data.append(chunk)
+ elif code == 3:
+ break
+ else:
+ raise T1Error('bad chunk code: ' + repr(code))
+ if onlyHeader:
+ break
data = bytesjoin(data)
assertType1(data)
return data
def readOther(path):
"""reads any (font) file, returns raw data"""
- f = open(path, "rb")
- data = f.read()
- f.close()
+ with open(path, "rb") as f:
+ data = f.read()
assertType1(data)
-
chunks = findEncryptedChunks(data)
data = []
for isEncrypted, chunk in chunks:
@@ -244,8 +241,7 @@ def writeLWFN(path, data):
def writePFB(path, data):
chunks = findEncryptedChunks(data)
- f = open(path, "wb")
- try:
+ with open(path, "wb") as f:
for isEncrypted, chunk in chunks:
if isEncrypted:
code = 2
@@ -255,13 +251,10 @@ def writePFB(path, data):
f.write(longToString(len(chunk)))
f.write(chunk)
f.write(bytechr(128) + bytechr(3))
- finally:
- f.close()
def writeOther(path, data, dohex=False):
chunks = findEncryptedChunks(data)
- f = open(path, "wb")
- try:
+ with open(path, "wb") as f:
hexlinelen = HEXLINELENGTH // 2
for isEncrypted, chunk in chunks:
if isEncrypted:
@@ -275,8 +268,6 @@ def writeOther(path, data, dohex=False):
chunk = chunk[hexlinelen:]
else:
f.write(chunk)
- finally:
- f.close()
# decryption tools
diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py
index 6dc48baf..1c11de72 100644
--- a/Lib/fontTools/ttLib/sfnt.py
+++ b/Lib/fontTools/ttLib/sfnt.py
@@ -123,6 +123,30 @@ class SFNTReader(object):
def close(self):
self.file.close()
+ def __deepcopy__(self, memo):
+ """Overrides the default deepcopy of SFNTReader object, to make it work
+ in the case when TTFont is loaded with lazy=True, and thus reader holds a
+ reference to a file object which is not pickleable.
+ We work around it by manually copying the data into a in-memory stream.
+ """
+ from copy import deepcopy
+
+ cls = self.__class__
+ obj = cls.__new__(cls)
+ for k, v in self.__dict__.items():
+ if k == "file":
+ pos = v.tell()
+ v.seek(0)
+ buf = BytesIO(v.read())
+ v.seek(pos)
+ buf.seek(pos)
+ if hasattr(v, "name"):
+ buf.name = v.name
+ obj.file = buf
+ else:
+ obj.__dict__[k] = deepcopy(v, memo)
+ return obj
+
# default compression level for WOFF 1.0 tables and metadata
ZLIB_COMPRESSION_LEVEL = 6
diff --git a/Lib/fontTools/ttLib/tables/F__e_a_t.py b/Lib/fontTools/ttLib/tables/F__e_a_t.py
index 22be4f67..ec497f22 100644
--- a/Lib/fontTools/ttLib/tables/F__e_a_t.py
+++ b/Lib/fontTools/ttLib/tables/F__e_a_t.py
@@ -58,8 +58,8 @@ class table_F__e_a_t(DefaultTable.DefaultTable):
fobj.default = vid
def compile(self, ttFont):
- fdat = ""
- vdat = ""
+ fdat = b""
+ vdat = b""
offset = 0
for f, v in sorted(self.features.items(), key=lambda x:x[1].index):
fnum = grUtils.tag2num(f)
diff --git a/Lib/fontTools/ttLib/tables/G__l_a_t.py b/Lib/fontTools/ttLib/tables/G__l_a_t.py
index 36ed6df3..7d1f7350 100644
--- a/Lib/fontTools/ttLib/tables/G__l_a_t.py
+++ b/Lib/fontTools/ttLib/tables/G__l_a_t.py
@@ -139,11 +139,11 @@ class table_G__l_a_t(DefaultTable.DefaultTable):
return data
def compileAttributes12(self, attrs, fmt):
- data = []
+ data = b""
for e in grUtils.entries(attrs):
- data.extend(sstruct.pack(fmt, {'attNum' : e[0], 'num' : e[1]}))
- data.extend(struct.pack(('>%dh' % len(e[2])), *e[2]))
- return "".join(data)
+ data += sstruct.pack(fmt, {'attNum' : e[0], 'num' : e[1]}) + \
+ struct.pack(('>%dh' % len(e[2])), *e[2])
+ return data
def compileAttributes3(self, attrs):
if self.hasOctaboxes:
@@ -168,7 +168,7 @@ class table_G__l_a_t(DefaultTable.DefaultTable):
vals = {}
for k in names:
if k == 'subboxBitmap': continue
- vals[k] = "{:.3f}%".format(getattr(o, k) * 100. / 256)
+ vals[k] = "{:.3f}%".format(getattr(o, k) * 100. / 255)
vals['bitmap'] = "{:0X}".format(o.subboxBitmap)
writer.begintag('octaboxes', **vals)
writer.newline()
@@ -176,7 +176,7 @@ class table_G__l_a_t(DefaultTable.DefaultTable):
for s in o.subboxes:
vals = {}
for k in names:
- vals[k] = "{:.3f}%".format(getattr(s, k) * 100. / 256)
+ vals[k] = "{:.3f}%".format(getattr(s, k) * 100. / 255)
writer.simpletag('octabox', **vals)
writer.newline()
writer.endtag('octaboxes')
@@ -190,6 +190,7 @@ class table_G__l_a_t(DefaultTable.DefaultTable):
def fromXML(self, name, attrs, content, ttFont):
if name == 'version' :
self.version = float(safeEval(attrs['version']))
+ self.scheme = int(safeEval(attrs['compressionScheme']))
if name != 'glyph' : return
if not hasattr(self, 'attributes'):
self.attributes = {}
@@ -209,13 +210,13 @@ class table_G__l_a_t(DefaultTable.DefaultTable):
o.subboxes = []
del attrs['bitmap']
for k, v in attrs.items():
- setattr(o, k, int(float(v[:-1]) * 256. / 100. + 0.5))
+ setattr(o, k, int(float(v[:-1]) * 255. / 100. + 0.5))
for element in subcontent:
if not isinstance(element, tuple): continue
(tag, attrs, subcontent) = element
so = _Object()
for k, v in attrs.items():
- setattr(so, k, int(float(v[:-1]) * 256. / 100. + 0.5))
+ setattr(so, k, int(float(v[:-1]) * 255. / 100. + 0.5))
o.subboxes.append(so)
attributes.octabox = o
self.attributes[gname] = attributes
diff --git a/Lib/fontTools/ttLib/tables/S__i_l_f.py b/Lib/fontTools/ttLib/tables/S__i_l_f.py
index 44dd69b0..e68b9b2e 100644
--- a/Lib/fontTools/ttLib/tables/S__i_l_f.py
+++ b/Lib/fontTools/ttLib/tables/S__i_l_f.py
@@ -6,6 +6,7 @@ from itertools import *
from . import DefaultTable
from . import grUtils
from array import array
+from functools import reduce
import struct, operator, warnings, re, sys
Silf_hdr_format = '''
@@ -220,23 +221,23 @@ def disassemble(aCode):
instre = re.compile("^\s*([^(]+)\s*(?:\(([^)]+)\))?")
def assemble(instrs):
- res = []
+ res = b""
for inst in instrs:
m = instre.match(inst)
if not m or not m.group(1) in aCode_map:
continue
opcode, parmfmt = aCode_map[m.group(1)]
- res.append(struct.pack("B", opcode))
+ res += struct.pack("B", opcode)
if m.group(2):
if parmfmt == 0:
continue
parms = [int(x) for x in re.split(",\s*", m.group(2))]
if parmfmt == -1:
l = len(parms)
- res.append(struct.pack(("%dB" % (l+1)), l, *parms))
+ res += struct.pack(("%dB" % (l+1)), l, *parms)
else:
- res.append(struct.pack(parmfmt, *parms))
- return b"".join(res)
+ res += struct.pack(parmfmt, *parms)
+ return res
def writecode(tag, writer, instrs):
writer.begintag(tag)
@@ -334,7 +335,7 @@ class table_S__i_l_f(DefaultTable.DefaultTable):
else:
hdr = sstruct.pack(Silf_hdr_format_3, self)
offset = len(hdr) + 4 * self.numSilf
- data = ""
+ data = b""
for s in self.silfs:
hdr += struct.pack(">L", offset)
subdata = s.compile(ttFont, self.version)
@@ -427,7 +428,7 @@ class Silf(object):
self.numJLevels = len(self.jLevels)
self.numCritFeatures = len(self.critFeatures)
numPseudo = len(self.pMap)
- data = ""
+ data = b""
if version >= 3.0:
hdroffset = sstruct.calcsize(Silf_part1_format_v3)
else:
@@ -453,8 +454,8 @@ class Silf(object):
u, ttFont.getGlyphID(p))
data1 += self.classes.compile(ttFont, version)
currpos += len(data1)
- data2 = ""
- datao = ""
+ data2 = b""
+ datao = b""
for i, p in enumerate(self.passes):
base = currpos + len(data2)
datao += struct.pack(">L", base)
@@ -464,7 +465,7 @@ class Silf(object):
if version >= 3.0:
data3 = sstruct.pack(Silf_part1_format_v3, self)
else:
- data3 = ""
+ data3 = b""
return data3 + data + datao + data1 + data2
@@ -592,8 +593,8 @@ class Classes(object):
oClasses = struct.unpack((">%dH" % (self.numClass+1)),
data[4:6+2*self.numClass])
for s,e in zip(oClasses[:self.numLinear], oClasses[1:self.numLinear+1]):
- self.linear.append(map(ttFont.getGlyphName,
- struct.unpack((">%dH" % ((e-s)/2)), data[s:e])))
+ self.linear.append(ttFont.getGlyphName(x) for x in
+ struct.unpack((">%dH" % ((e-s)/2)), data[s:e]))
for s,e in zip(oClasses[self.numLinear:self.numClass],
oClasses[self.numLinear+1:self.numClass+1]):
nonLinids = [struct.unpack(">HH", data[x:x+4]) for x in range(s+8, e, 4)]
@@ -601,7 +602,7 @@ class Classes(object):
self.nonLinear.append(nonLin)
def compile(self, ttFont, version=2.0):
- data = ""
+ data = b""
oClasses = []
if version >= 4.0:
offset = 8 + 4 * (len(self.linear) + len(self.nonLinear))
@@ -609,13 +610,13 @@ class Classes(object):
offset = 6 + 2 * (len(self.linear) + len(self.nonLinear))
for l in self.linear:
oClasses.append(len(data) + offset)
- gs = map(ttFont.getGlyphID, l)
+ gs = [ttFont.getGlyphID(x) for x in l]
data += struct.pack((">%dH" % len(l)), *gs)
for l in self.nonLinear:
oClasses.append(len(data) + offset)
gs = [(ttFont.getGlyphID(x[0]), x[1]) for x in l.items()]
data += grUtils.bininfo(len(gs))
- data += "".join([struct.pack(">HH", *x) for x in sorted(gs)])
+ data += b"".join([struct.pack(">HH", *x) for x in sorted(gs)])
oClasses.append(len(data) + offset)
self.numClass = len(oClasses) - 1
self.numLinear = len(self.linear)
@@ -680,7 +681,7 @@ class Pass(object):
self.rulePreContexts = []
self.ruleSortKeys = []
self.ruleConstraints = []
- self.passConstraints = ""
+ self.passConstraints = b""
self.actions = []
self.stateTrans = []
self.startStates = []
@@ -725,7 +726,7 @@ class Pass(object):
for i in range(len(oConstraints)-2,-1,-1):
if oConstraints[i] == 0 :
oConstraints[i] = oConstraints[i+1]
- self.ruleConstraints = [(data[s:e] if (e-s > 1) else "") for (s,e) in zip(oConstraints, oConstraints[1:])]
+ self.ruleConstraints = [(data[s:e] if (e-s > 1) else b"") for (s,e) in zip(oConstraints, oConstraints[1:])]
data = data[oConstraints[-1]:]
self.actions = [(data[s:e] if (e-s > 1) else "") for (s,e) in zip(oActions, oActions[1:])]
data = data[oActions[-1]:]
@@ -733,9 +734,9 @@ class Pass(object):
def compile(self, ttFont, base, version=2.0):
# build it all up backwards
- oActions = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.actions + [""], (0, []))[1]
- oConstraints = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.ruleConstraints + [""], (1, []))[1]
- constraintCode = "\000" + "".join(self.ruleConstraints)
+ oActions = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.actions + [b""], (0, []))[1]
+ oConstraints = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.ruleConstraints + [b""], (1, []))[1]
+ constraintCode = b"\000" + b"".join(self.ruleConstraints)
transes = []
for t in self.stateTrans:
if sys.byteorder != "big": t.byteswap()
@@ -761,7 +762,7 @@ class Pass(object):
# now generate output
data = sstruct.pack(Silf_pass_format, self)
data += grUtils.bininfo(len(passRanges), 6)
- data += "".join(struct.pack(">3H", *p) for p in passRanges)
+ data += b"".join(struct.pack(">3H", *p) for p in passRanges)
data += struct.pack((">%dH" % len(oRuleMap)), *oRuleMap)
flatrules = reduce(lambda a,x: a+x, self.rules, [])
data += struct.pack((">%dH" % oRuleMap[-1]), *flatrules)
@@ -772,8 +773,8 @@ class Pass(object):
data += struct.pack(">BH", self.collisionThreshold, len(self.passConstraints))
data += struct.pack((">%dH" % (self.numRules+1)), *oConstraints)
data += struct.pack((">%dH" % (self.numRules+1)), *oActions)
- return data + "".join(transes) + struct.pack("B", 0) + \
- self.passConstraints + constraintCode + "".join(self.actions)
+ return data + b"".join(transes) + struct.pack("B", 0) + \
+ self.passConstraints + constraintCode + b"".join(self.actions)
def toXML(self, writer, ttFont, version=2.0):
writesimple('info', self, writer, *pass_attrs_info)
@@ -839,7 +840,7 @@ class Pass(object):
if not isinstance(e, tuple): continue
tag, a, c = e
if tag == 'state':
- self.rules.append(map(int, a['rules'].split(" ")))
+ self.rules.append([int(x) for x in a['rules'].split(" ")])
elif name == 'rules':
for element in content:
if not isinstance(element, tuple): continue
@@ -847,8 +848,8 @@ class Pass(object):
if tag != 'rule': continue
self.rulePreContexts.append(int(a['precontext']))
self.ruleSortKeys.append(int(a['sortkey']))
- con = ""
- act = ""
+ con = b""
+ act = b""
for e in c:
if not isinstance(e, tuple): continue
tag, a, subc = e
diff --git a/Lib/fontTools/ttLib/tables/S__i_l_l.py b/Lib/fontTools/ttLib/tables/S__i_l_l.py
index 7acef1df..4671e137 100644
--- a/Lib/fontTools/ttLib/tables/S__i_l_l.py
+++ b/Lib/fontTools/ttLib/tables/S__i_l_l.py
@@ -42,8 +42,8 @@ class table_S__i_l_l(DefaultTable.DefaultTable):
self.langs[c].append(finfo[i])
def compile(self, ttFont):
- ldat = ""
- fdat = ""
+ ldat = b""
+ fdat = b""
offset = 0
for c, inf in sorted(self.langs.items()):
ldat += struct.pack(">4sHH", c.encode('utf8'), len(inf), 8 * (offset + len(self.langs) + 1))
diff --git a/Lib/fontTools/ttLib/tables/_k_e_r_n.py b/Lib/fontTools/ttLib/tables/_k_e_r_n.py
index 98eb7092..1e5140bc 100644
--- a/Lib/fontTools/ttLib/tables/_k_e_r_n.py
+++ b/Lib/fontTools/ttLib/tables/_k_e_r_n.py
@@ -48,6 +48,19 @@ class table__k_e_r_n(DefaultTable.DefaultTable):
# This "version" is always 0 so we ignore it here
_, length, subtableFormat, coverage = struct.unpack(
">HHBB", data[:6])
+ if nTables == 1 and subtableFormat == 0:
+ # The "length" value is ignored since some fonts
+ # (like OpenSans and Calibri) have a subtable larger than
+ # its value.
+ nPairs, = struct.unpack(">H", data[6:8])
+ calculated_length = (nPairs * 6) + 14
+ if length != calculated_length:
+ log.warning(
+ "'kern' subtable longer than defined: "
+ "%d bytes instead of %d bytes" %
+ (calculated_length, length)
+ )
+ length = calculated_length
if subtableFormat not in kern_classes:
subtable = KernTable_format_unkown(subtableFormat)
else:
@@ -128,7 +141,6 @@ class KernTable_format_0(object):
">HHHH", data[:8])
data = data[8:]
- nPairs = min(nPairs, len(data) // 6)
datas = array.array("H", data[:6 * nPairs])
if sys.byteorder != "big": datas.byteswap()
it = iter(datas)
@@ -153,6 +165,7 @@ class KernTable_format_0(object):
def compile(self, ttFont):
nPairs = len(self.kernTable)
searchRange, entrySelector, rangeShift = getSearchRange(nPairs, 6)
+ searchRange &= 0xFFFF
data = struct.pack(
">HHHH", nPairs, searchRange, entrySelector, rangeShift)
@@ -175,6 +188,10 @@ class KernTable_format_0(object):
if not self.apple:
version = 0
length = len(data) + 6
+ if length >= 0x10000:
+ log.warning('"kern" subtable overflow, '
+ 'truncating length value while preserving pairs.')
+ length &= 0xFFFF
header = struct.pack(
">HHBB", version, length, self.format, self.coverage)
else:
diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py
index a30291cc..488c4ea5 100644
--- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py
+++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py
@@ -161,7 +161,8 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
raise ValueError("nameID must be less than 32768")
return nameID
- def addMultilingualName(self, names, ttFont=None, nameID=None):
+ def addMultilingualName(self, names, ttFont=None, nameID=None,
+ windows=True, mac=True):
"""Add a multilingual name, returning its name ID
'names' is a dictionary with the name in multiple languages,
@@ -176,6 +177,9 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
'nameID' is the name ID to be used, or None to let the library
pick an unused name ID.
+
+ If 'windows' is True, a platformID=3 name record will be added.
+ If 'mac' is True, a platformID=1 name record will be added.
"""
if not hasattr(self, 'names'):
self.names = []
@@ -184,15 +188,16 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
# TODO: Should minimize BCP 47 language codes.
# https://github.com/fonttools/fonttools/issues/930
for lang, name in sorted(names.items()):
- # Apple platforms have been recognizing Windows names
- # since early OSX (~2001), so we only add names
- # for the Macintosh platform when we cannot not make
- # a Windows name. This can happen for exotic BCP47
- # language tags that have no Windows language code.
- windowsName = _makeWindowsName(name, nameID, lang)
- if windowsName is not None:
- self.names.append(windowsName)
- else:
+ if windows:
+ windowsName = _makeWindowsName(name, nameID, lang)
+ if windowsName is not None:
+ self.names.append(windowsName)
+ else:
+ # We cannot not make a Windows name: make sure we add a
+ # Mac name as a fallback. This can happen for exotic
+ # BCP47 language tags that have no Windows language code.
+ mac = True
+ if mac:
macName = _makeMacName(name, nameID, lang, ttFont)
if macName is not None:
self.names.append(macName)
diff --git a/Lib/fontTools/ttLib/tables/grUtils.py b/Lib/fontTools/ttLib/tables/grUtils.py
index 1ce2c9e9..d11ac4b1 100644
--- a/Lib/fontTools/ttLib/tables/grUtils.py
+++ b/Lib/fontTools/ttLib/tables/grUtils.py
@@ -13,7 +13,7 @@ def decompress(data):
if scheme == 0:
pass
elif scheme == 1 and lz4:
- res = lz4.decompress(struct.pack("<L", size) + data[8:])
+ res = lz4.block.decompress(struct.pack("<L", size) + data[8:])
if len(res) != size:
warnings.warn("Table decompression failed.")
else:
@@ -27,8 +27,8 @@ def compress(scheme, data):
if scheme == 0 :
return data
elif scheme == 1 and lz4:
- res = lz4.compress(hdr + data)
- return res
+ res = lz4.block.compress(data, mode='high_compression', compression=16, store_size=False)
+ return hdr + res
else:
warnings.warn("Table failed to compress by unsupported compression scheme")
return data
@@ -47,7 +47,7 @@ def _entries(attrs, sameval):
yield (ak - len(vals) + 1, len(vals), vals)
def entries(attributes, sameval = False):
- g = _entries(sorted(attributes.iteritems(), key=lambda x:int(x[0])), sameval)
+ g = _entries(sorted(attributes.items(), key=lambda x:int(x[0])), sameval)
return g
def bininfo(num, size=1):
@@ -59,7 +59,7 @@ def bininfo(num, size=1):
srange *= 2
select += 1
select -= 1
- srange /= 2
+ srange //= 2
srange *= size
shift = num * size - srange
return struct.pack(">4H", num, srange, select, shift)
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index d08bcc57..d08bcc57 100755..100644
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
diff --git a/Lib/fontTools/ttLib/tables/ttProgram.py b/Lib/fontTools/ttLib/tables/ttProgram.py
index 182982f2..7ffb37a0 100644
--- a/Lib/fontTools/ttLib/tables/ttProgram.py
+++ b/Lib/fontTools/ttLib/tables/ttProgram.py
@@ -63,6 +63,7 @@ instructions = [
(0x66, 'FLOOR', 0, 'Floor', 1, 1), # n floor(n)
(0x46, 'GC', 1, 'GetCoordOnPVector', 1, 1), # p c
(0x88, 'GETINFO', 0, 'GetInfo', 1, 1), # selector result
+ (0x91, 'GETVARIATION', 0, 'GetVariation', 0, -1), # - a1,..,an
(0x0d, 'GFV', 0, 'GetFVector', 0, 2), # - px, py
(0x0c, 'GPV', 0, 'GetPVector', 0, 2), # - px, py
(0x52, 'GT', 0, 'GreaterThan', 2, 1), # e2, e1 b
@@ -158,7 +159,7 @@ def bitRepr(value, bits):
return s
-_mnemonicPat = re.compile("[A-Z][A-Z0-9]*$")
+_mnemonicPat = re.compile(r"[A-Z][A-Z0-9]*$")
def _makeDict(instructionList):
opcodeDict = {}
@@ -194,8 +195,8 @@ _whiteRE = re.compile(r"\s*")
_pushCountPat = re.compile(r"[A-Z][A-Z0-9]*\s*\[.*?\]\s*/\* ([0-9]+).*?\*/")
-_indentRE = re.compile("^FDEF|IF|ELSE\[ \]\t.+")
-_unindentRE = re.compile("^ELSE|ENDF|EIF\[ \]\t.+")
+_indentRE = re.compile(r"^FDEF|IF|ELSE\[ \]\t.+")
+_unindentRE = re.compile(r"^ELSE|ENDF|EIF\[ \]\t.+")
def _skipWhite(data, pos):
m = _whiteRE.match(data, pos)
diff --git a/Lib/fontTools/ttx.py b/Lib/fontTools/ttx.py
index 0e3eaf39..a785325e 100644
--- a/Lib/fontTools/ttx.py
+++ b/Lib/fontTools/ttx.py
@@ -293,11 +293,11 @@ def ttCompile(input, output, options):
def guessFileType(fileName):
base, ext = os.path.splitext(fileName)
try:
- f = open(fileName, "rb")
+ with open(fileName, "rb") as f:
+ header = f.read(256)
except IOError:
return None
- header = f.read(256)
- f.close()
+
if header.startswith(b'\xef\xbb\xbf<?xml'):
header = header.lstrip(b'\xef\xbb\xbf')
cr, tp = getMacCreatorAndType(fileName)
diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py
index b1ece9cf..d9a57c5d 100755..100644
--- a/Lib/fontTools/ufoLib/__init__.py
+++ b/Lib/fontTools/ufoLib/__init__.py
@@ -5,6 +5,7 @@ from copy import deepcopy
import logging
import zipfile
import enum
+from collections import OrderedDict
import fs
import fs.base
import fs.subfs
@@ -57,6 +58,7 @@ __all__ = [
"UFOLibError",
"UFOReader",
"UFOWriter",
+ "UFOReaderWriter",
"UFOFileStructure",
"fontInfoAttributesVersion1",
"fontInfoAttributesVersion2",
@@ -105,104 +107,92 @@ class UFOFileStructure(enum.Enum):
# --------------
-def _getFileModificationTime(self, path):
- """
- Returns the modification time for the file at the given path, as a
- floating point number giving the number of seconds since the epoch.
- The path must be relative to the UFO path.
- Returns None if the file does not exist.
- """
- try:
- dt = self.fs.getinfo(fsdecode(path), namespaces=["details"]).modified
- except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound):
- return None
- else:
- return datetimeAsTimestamp(dt)
-
-
-def _readBytesFromPath(self, path):
- """
- Returns the bytes in the file at the given path.
- The path must be relative to the UFO's filesystem root.
- Returns None if the file does not exist.
- """
- try:
- return self.fs.getbytes(fsdecode(path))
- except fs.errors.ResourceNotFound:
- return None
+class _UFOBaseIO(object):
-
-def _getPlist(self, fileName, default=None):
- """
- Read a property list relative to the UFO filesystem's root.
- Raises UFOLibError if the file is missing and default is None,
- otherwise default is returned.
-
- The errors that could be raised during the reading of a plist are
- unpredictable and/or too large to list, so, a blind try: except:
- is done. If an exception occurs, a UFOLibError will be raised.
- """
- try:
- with self.fs.open(fileName, "rb") as f:
- return plistlib.load(f)
- except fs.errors.ResourceNotFound:
- if default is None:
- raise UFOLibError(
- "'%s' is missing on %s. This file is required"
- % (fileName, self.fs)
- )
+ def getFileModificationTime(self, path):
+ """
+ Returns the modification time for the file at the given path, as a
+ floating point number giving the number of seconds since the epoch.
+ The path must be relative to the UFO path.
+ Returns None if the file does not exist.
+ """
+ try:
+ dt = self.fs.getinfo(fsdecode(path), namespaces=["details"]).modified
+ except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound):
+ return None
else:
- return default
- except Exception as e:
- # TODO(anthrotype): try to narrow this down a little
- raise UFOLibError(
- "'%s' could not be read on %s: %s" % (fileName, self.fs, e)
- )
-
-
-def _writePlist(self, fileName, obj):
- """
- Write a property list to a file relative to the UFO filesystem's root.
+ return datetimeAsTimestamp(dt)
- Do this sort of atomically, making it harder to corrupt existing files,
- for example when plistlib encounters an error halfway during write.
- This also checks to see if text matches the text that is already in the
- file at path. If so, the file is not rewritten so that the modification
- date is preserved.
+ def _getPlist(self, fileName, default=None):
+ """
+ Read a property list relative to the UFO filesystem's root.
+ Raises UFOLibError if the file is missing and default is None,
+ otherwise default is returned.
- The errors that could be raised during the writing of a plist are
- unpredictable and/or too large to list, so, a blind try: except: is done.
- If an exception occurs, a UFOLibError will be raised.
- """
- if self._havePreviousFile:
+ The errors that could be raised during the reading of a plist are
+ unpredictable and/or too large to list, so, a blind try: except:
+ is done. If an exception occurs, a UFOLibError will be raised.
+ """
try:
- data = plistlib.dumps(obj)
+ with self.fs.open(fileName, "rb") as f:
+ return plistlib.load(f)
+ except fs.errors.ResourceNotFound:
+ if default is None:
+ raise UFOLibError(
+ "'%s' is missing on %s. This file is required"
+ % (fileName, self.fs)
+ )
+ else:
+ return default
except Exception as e:
+ # TODO(anthrotype): try to narrow this down a little
raise UFOLibError(
- "'%s' could not be written on %s because "
- "the data is not properly formatted: %s"
- % (fileName, self.fs, e)
+ "'%s' could not be read on %s: %s" % (fileName, self.fs, e)
)
- if self.fs.exists(fileName) and data == self.fs.getbytes(fileName):
- return
- self.fs.setbytes(fileName, data)
- else:
- with self.fs.openbin(fileName, mode="w") as fp:
+
+ def _writePlist(self, fileName, obj):
+ """
+ Write a property list to a file relative to the UFO filesystem's root.
+
+ Do this sort of atomically, making it harder to corrupt existing files,
+ for example when plistlib encounters an error halfway during write.
+ This also checks to see if text matches the text that is already in the
+ file at path. If so, the file is not rewritten so that the modification
+ date is preserved.
+
+ The errors that could be raised during the writing of a plist are
+ unpredictable and/or too large to list, so, a blind try: except: is done.
+ If an exception occurs, a UFOLibError will be raised.
+ """
+ if self._havePreviousFile:
try:
- plistlib.dump(obj, fp)
+ data = plistlib.dumps(obj)
except Exception as e:
raise UFOLibError(
"'%s' could not be written on %s because "
"the data is not properly formatted: %s"
% (fileName, self.fs, e)
)
+ if self.fs.exists(fileName) and data == self.fs.getbytes(fileName):
+ return
+ self.fs.setbytes(fileName, data)
+ else:
+ with self.fs.openbin(fileName, mode="w") as fp:
+ try:
+ plistlib.dump(obj, fp)
+ except Exception as e:
+ raise UFOLibError(
+ "'%s' could not be written on %s because "
+ "the data is not properly formatted: %s"
+ % (fileName, self.fs, e)
+ )
# ----------
# UFO Reader
# ----------
-class UFOReader(object):
+class UFOReader(_UFOBaseIO):
"""
Read the various components of the .ufo.
@@ -280,6 +270,18 @@ class UFOReader(object):
# properties
+ def _get_path(self):
+ import warnings
+
+ warnings.warn(
+ "The 'path' attribute is deprecated; use the 'fs' attribute instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._path
+
+ path = property(_get_path, doc="The path of the UFO (DEPRECATED).")
+
def _get_formatVersion(self):
return self._formatVersion
@@ -291,7 +293,7 @@ class UFOReader(object):
fileStructure = property(
_get_fileStructure,
doc=(
- "The current file structure of the UFO: "
+ "The file structure of the UFO: "
"either UFOFileStructure.ZIP or UFOFileStructure.PACKAGE"
)
)
@@ -347,9 +349,16 @@ class UFOReader(object):
# support methods
- _getPlist = _getPlist
- getFileModificationTime = _getFileModificationTime
- readBytesFromPath = _readBytesFromPath
+ def readBytesFromPath(self, path):
+ """
+ Returns the bytes in the file at the given path.
+ The path must be relative to the UFO's filesystem root.
+ Returns None if the file does not exist.
+ """
+ try:
+ return self.fs.getbytes(fsdecode(path))
+ except fs.errors.ResourceNotFound:
+ return None
def getReadFileForPath(self, path, encoding=None):
"""
@@ -796,7 +805,7 @@ class UFOReader(object):
# UFO Writer
# ----------
-class UFOWriter(object):
+class UFOWriter(UFOReader):
"""
Write the various components of the .ufo.
@@ -821,6 +830,8 @@ class UFOWriter(object):
path = path.__fspath__()
if isinstance(path, basestring):
+ # normalize path by removing trailing or double slashes
+ path = os.path.normpath(path)
havePreviousFile = os.path.exists(path)
if havePreviousFile:
# ensure we use the same structure as the destination
@@ -946,7 +957,7 @@ class UFOWriter(object):
self.layerContents = {}
if previousFormatVersion is not None and previousFormatVersion >= 3:
# already exists
- self._readLayerContents(validate=validate)
+ self.layerContents = OrderedDict(self._readLayerContents(validate))
else:
# previous < 3
# imply the layer contents
@@ -957,46 +968,13 @@ class UFOWriter(object):
# properties
- def _get_path(self):
- import warnings
-
- warnings.warn(
- "The 'path' attribute is deprecated; use the 'fs' attribute instead",
- DeprecationWarning,
- stacklevel=2,
- )
- return self._path
-
- path = property(_get_path, doc="The path the UFO is being written to (DEPRECATED).")
-
- def _get_formatVersion(self):
- return self._formatVersion
-
- formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is set into metainfo.plist during __init__.")
-
def _get_fileCreator(self):
return self._fileCreator
fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.")
- def _get_fileStructure(self):
- return self._fileStructure
-
- fileStructure = property(
- _get_fileStructure,
- doc=(
- "The file structure of the destination UFO: "
- "either UFOFileStrucure.ZIP or UFOFileStructure.PACKAGE"
- )
- )
-
# support methods for file system interaction
- _getPlist = _getPlist
- _writePlist = _writePlist
- readBytesFromPath = _readBytesFromPath
- getFileModificationTime = _getFileModificationTime
-
def copyFromReader(self, reader, sourcePath, destPath):
"""
Copy the sourcePath in the provided UFOReader to destPath
@@ -1332,25 +1310,6 @@ class UFOWriter(object):
# glyph sets & layers
- def _readLayerContents(self, validate):
- """
- Rebuild the layer contents list by checking what glyph sets
- are available on disk.
-
- ``validate`` will validate the data.
- """
- # read the file on disk
- raw = self._getPlist(LAYERCONTENTS_FILENAME)
- contents = {}
- if validate:
- valid, error = layerContentsValidator(raw, self.fs)
- if not valid:
- raise UFOLibError(error)
- for entry in raw:
- layerName, directoryName = entry
- contents[layerName] = directoryName
- self.layerContents = contents
-
def writeLayerContents(self, layerOrder=None, validate=None):
"""
Write the layercontents.plist file. This method *must* be called
@@ -1412,7 +1371,7 @@ class UFOWriter(object):
raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion)
# locate a layer name when None has been given
if layerName is None and defaultLayer:
- for existingLayerName, directory in list(self.layerContents.items()):
+ for existingLayerName, directory in self.layerContents.items():
if directory == DEFAULT_GLYPHS_DIRNAME:
layerName = existingLayerName
if layerName is None:
@@ -1460,10 +1419,13 @@ class UFOWriter(object):
# matches the default being written. also make sure that this layer
# name is not already linked to a non-default layer.
if defaultLayer:
- for existingLayerName, directory in list(self.layerContents.items()):
+ for existingLayerName, directory in self.layerContents.items():
if directory == DEFAULT_GLYPHS_DIRNAME:
if existingLayerName != layerName:
- raise UFOLibError("Another layer is already mapped to the default directory.")
+ raise UFOLibError(
+ "Another layer ('%s') is already mapped to the default directory."
+ % existingLayerName
+ )
elif existingLayerName == layerName:
raise UFOLibError("The layer name is already mapped to a non-default layer.")
# get an existing directory name
@@ -1476,7 +1438,7 @@ class UFOWriter(object):
else:
# not caching this could be slightly expensive,
# but caching it will be cumbersome
- existing = [d.lower() for d in list(self.layerContents.values())]
+ existing = {d.lower() for d in self.layerContents.values()}
if not isinstance(layerName, unicode):
try:
layerName = unicode(layerName)
@@ -1524,14 +1486,14 @@ class UFOWriter(object):
if newLayerName in self.layerContents:
raise UFOLibError("A layer named %s already exists." % newLayerName)
# make sure the default layer doesn't already exist
- if defaultLayer and DEFAULT_GLYPHS_DIRNAME in list(self.layerContents.values()):
+ if defaultLayer and DEFAULT_GLYPHS_DIRNAME in self.layerContents.values():
raise UFOLibError("A default layer already exists.")
# get the paths
oldDirectory = self._findDirectoryForLayerName(layerName)
if defaultLayer:
newDirectory = DEFAULT_GLYPHS_DIRNAME
else:
- existing = [name.lower() for name in list(self.layerContents.values())]
+ existing = {name.lower() for name in self.layerContents.values()}
newDirectory = userNameToFileName(newLayerName, existing=existing, prefix="glyphs.")
# update the internal mapping
del self.layerContents[layerName]
@@ -1612,14 +1574,11 @@ class UFOWriter(object):
rootDir = os.path.splitext(os.path.basename(self._path))[0] + ".ufo"
with fs.zipfs.ZipFS(self._path, write=True, encoding="utf-8") as destFS:
fs.copy.copy_fs(self.fs, destFS.makedir(rootDir))
- if self._shouldClose:
- self.fs.close()
+ super(UFOWriter, self).close()
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_value, exc_tb):
- self.close()
+# just an alias, makes it more explicit
+UFOReaderWriter = UFOWriter
# ----------------
diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py
index eae300b2..f2648b8c 100755..100644
--- a/Lib/fontTools/ufoLib/glifLib.py
+++ b/Lib/fontTools/ufoLib/glifLib.py
@@ -34,6 +34,7 @@ from fontTools.ufoLib.validators import (
glyphLibValidator,
)
from fontTools.misc import etree
+from fontTools.ufoLib import _UFOBaseIO
from fontTools.ufoLib.utils import integerTypes, numberTypes
@@ -88,7 +89,7 @@ class Glyph(object):
# Glyph Set
# ---------
-class GlyphSet(object):
+class GlyphSet(_UFOBaseIO):
"""
GlyphSet manages a set of .glif files inside one directory.
@@ -169,9 +170,6 @@ class GlyphSet(object):
self.rebuildContents()
- # here we reuse the same methods from UFOReader/UFOWriter
- from fontTools.ufoLib import _getPlist, _writePlist, _getFileModificationTime
-
def rebuildContents(self, validateRead=None):
"""
Rebuild the contents dict by loading contents.plist.
@@ -308,7 +306,7 @@ class GlyphSet(object):
Raises KeyError if the glyphName is not in contents.plist.
"""
fileName = self.contents[glyphName]
- return self._getFileModificationTime(fileName)
+ return self.getFileModificationTime(fileName)
# reading/writing API
diff --git a/Lib/fontTools/unicode.py b/Lib/fontTools/unicode.py
index 50dfc539..ef3ed695 100644
--- a/Lib/fontTools/unicode.py
+++ b/Lib/fontTools/unicode.py
@@ -18,8 +18,11 @@ class _UnicodeCustom(object):
def __init__(self, f):
if isinstance(f, basestring):
- f = open(f)
- self.codes = _makeunicodes(f)
+ with open(f) as fd:
+ codes = _makeunicodes(fd)
+ else:
+ codes = _makeunicodes(f)
+ self.codes = codes
def __getitem__(self, charCode):
try:
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index 437324e0..37d8d334 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -23,7 +23,7 @@ from __future__ import unicode_literals
from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import otRound
from fontTools.misc.arrayTools import Vector
-from fontTools.ttLib import TTFont, newTable
+from fontTools.ttLib import TTFont, newTable, TTLibError
from fontTools.ttLib.tables._n_a_m_e import NameRecord
from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
@@ -32,7 +32,7 @@ from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import OTTableWriter
from fontTools.varLib import builder, models, varStore
-from fontTools.varLib.merger import VariationMerger, _all_equal
+from fontTools.varLib.merger import VariationMerger
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.iup import iup_delta_optimize
from fontTools.varLib.featureVars import addFeatureVariations
@@ -40,6 +40,7 @@ from fontTools.designspaceLib import DesignSpaceDocument, AxisDescriptor
from collections import OrderedDict, namedtuple
import os.path
import logging
+from copy import deepcopy
from pprint import pformat
log = logging.getLogger("fontTools.varLib")
@@ -184,7 +185,7 @@ def _add_stat(font, axes):
STAT = font["STAT"] = newTable('STAT')
stat = STAT.table = ot.STAT()
- stat.Version = 0x00010002
+ stat.Version = 0x00010001
axisRecords = []
for i, a in enumerate(fvarTable.axes):
@@ -280,7 +281,7 @@ def _SetCoordinates(font, glyphName, coord):
# XXX Handle vertical
font["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing
-def _add_gvar(font, model, master_ttfs, tolerance=0.5, optimize=True):
+def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
assert tolerance >= 0
@@ -291,13 +292,19 @@ def _add_gvar(font, model, master_ttfs, tolerance=0.5, optimize=True):
gvar.reserved = 0
gvar.variations = {}
+ glyf = font['glyf']
+
for glyph in font.getGlyphOrder():
+ isComposite = glyf[glyph].isComposite()
+
allData = [_GetCoordinates(m, glyph) for m in master_ttfs]
+ model, allData = masterModel.getSubModel(allData)
+
allCoords = [d[0] for d in allData]
allControls = [d[1] for d in allData]
control = allControls[0]
- if (any(c != control for c in allControls)):
+ if not models.allEqual(allControls):
log.warning("glyph %s has incompatible masters; skipping" % glyph)
continue
del allControls
@@ -313,13 +320,23 @@ def _add_gvar(font, model, master_ttfs, tolerance=0.5, optimize=True):
endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])):
- if all(abs(v) <= tolerance for v in delta.array):
+ if all(abs(v) <= tolerance for v in delta.array) and not isComposite:
continue
var = TupleVariation(support, delta)
if optimize:
delta_opt = iup_delta_optimize(delta, origCoords, endPts, tolerance=tolerance)
if None in delta_opt:
+ """In composite glyphs, there should be one 0 entry
+ to make sure the gvar entry is written to the font.
+
+ This is to work around an issue with macOS 10.14 and can be
+ removed once the behaviour of macOS is changed.
+
+ https://github.com/fonttools/fonttools/issues/1381
+ """
+ if all(d is None for d in delta_opt):
+ delta_opt = [(0, 0)] + [None] * (len(delta_opt) - 1)
# Use "optimized" version only if smaller...
var_opt = TupleVariation(support, delta_opt)
@@ -344,7 +361,7 @@ def _remove_TTHinting(font):
font["glyf"].removeHinting()
# TODO: Modify gasp table to deactivate gridfitting for all ranges?
-def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5):
+def _merge_TTHinting(font, masterModel, master_ttfs, tolerance=0.5):
log.info("Merging TT hinting")
assert "cvar" not in font
@@ -372,7 +389,7 @@ def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5):
all_pgms = [
m["glyf"][name].program
for m in master_ttfs
- if hasattr(m["glyf"][name], "program")
+ if name in m['glyf'] and hasattr(m["glyf"][name], "program")
]
if not any(all_pgms):
continue
@@ -383,24 +400,21 @@ def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5):
font_pgm = Program()
if any(pgm != font_pgm for pgm in all_pgms if pgm):
log.warning("Masters have incompatible glyph programs in glyph '%s', hinting is discarded." % name)
+ # TODO Only drop hinting from this glyph.
_remove_TTHinting(font)
return
# cvt table
- all_cvs = [Vector(m["cvt "].values) for m in master_ttfs if "cvt " in m]
-
- if len(all_cvs) == 0:
- # There is no cvt table to make a cvar table from, we're done here.
- return
+ all_cvs = [Vector(m["cvt "].values) if 'cvt ' in m else None
+ for m in master_ttfs]
- if len(all_cvs) != len(master_ttfs):
- log.warning("Some masters have no cvt table, hinting is discarded.")
- _remove_TTHinting(font)
+ nonNone_cvs = models.nonNone(all_cvs)
+ if not nonNone_cvs:
+ # There is no cvt table to make a cvar table from, we're done here.
return
- num_cvt0 = len(all_cvs[0])
- if (any(len(c) != num_cvt0 for c in all_cvs)):
+ if not models.allEqual(len(c) for c in nonNone_cvs):
log.warning("Masters have incompatible cvt tables, hinting is discarded.")
_remove_TTHinting(font)
return
@@ -411,8 +425,7 @@ def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5):
cvar.version = 1
cvar.variations = []
- deltas = model.getDeltas(all_cvs)
- supports = model.supports
+ deltas, supports = masterModel.getDeltasAndSupports(all_cvs)
for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])):
delta = [otRound(d) for d in delta]
if all(abs(v) <= tolerance for v in delta):
@@ -420,54 +433,59 @@ def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5):
var = TupleVariation(support, delta)
cvar.variations.append(var)
-def _add_HVAR(font, model, master_ttfs, axisTags):
+def _add_HVAR(font, masterModel, master_ttfs, axisTags):
log.info("Generating HVAR")
- hAdvanceDeltas = {}
+ glyphOrder = font.getGlyphOrder()
+
+ hAdvanceDeltasAndSupports = {}
metricses = [m["hmtx"].metrics for m in master_ttfs]
- for glyph in font.getGlyphOrder():
- hAdvances = [metrics[glyph][0] for metrics in metricses]
- # TODO move round somewhere else?
- hAdvanceDeltas[glyph] = tuple(otRound(d) for d in model.getDeltas(hAdvances)[1:])
-
- # Direct mapping
- supports = model.supports[1:]
- varTupleList = builder.buildVarRegionList(supports, axisTags)
- varTupleIndexes = list(range(len(supports)))
- n = len(supports)
- items = []
- for glyphName in font.getGlyphOrder():
- items.append(hAdvanceDeltas[glyphName])
-
- # Build indirect mapping to save on duplicates, compare both sizes
- uniq = list(set(items))
- mapper = {v:i for i,v in enumerate(uniq)}
- mapping = [mapper[item] for item in items]
- advanceMapping = builder.buildVarIdxMap(mapping, font.getGlyphOrder())
-
- # Direct
- varData = builder.buildVarData(varTupleIndexes, items)
- directStore = builder.buildVarStore(varTupleList, [varData])
-
- # Indirect
- varData = builder.buildVarData(varTupleIndexes, uniq)
- indirectStore = builder.buildVarStore(varTupleList, [varData])
- mapping = indirectStore.optimize()
- advanceMapping.mapping = {k:mapping[v] for k,v in advanceMapping.mapping.items()}
-
- # Compile both, see which is more compact
-
- writer = OTTableWriter()
- directStore.compile(writer, font)
- directSize = len(writer.getAllData())
-
- writer = OTTableWriter()
- indirectStore.compile(writer, font)
- advanceMapping.compile(writer, font)
- indirectSize = len(writer.getAllData())
-
- use_direct = directSize < indirectSize
+ for glyph in glyphOrder:
+ hAdvances = [metrics[glyph][0] if glyph in metrics else None for metrics in metricses]
+ hAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(hAdvances)
+
+ singleModel = models.allEqual(id(v[1]) for v in hAdvanceDeltasAndSupports.values())
+
+ directStore = None
+ if singleModel:
+ # Build direct mapping
+
+ supports = next(iter(hAdvanceDeltasAndSupports.values()))[1][1:]
+ varTupleList = builder.buildVarRegionList(supports, axisTags)
+ varTupleIndexes = list(range(len(supports)))
+ varData = builder.buildVarData(varTupleIndexes, [], optimize=False)
+ for glyphName in glyphOrder:
+ varData.addItem(hAdvanceDeltasAndSupports[glyphName][0])
+ varData.optimize()
+ directStore = builder.buildVarStore(varTupleList, [varData])
+
+ # Build optimized indirect mapping
+ storeBuilder = varStore.OnlineVarStoreBuilder(axisTags)
+ mapping = {}
+ for glyphName in glyphOrder:
+ deltas,supports = hAdvanceDeltasAndSupports[glyphName]
+ storeBuilder.setSupports(supports)
+ mapping[glyphName] = storeBuilder.storeDeltas(deltas)
+ indirectStore = storeBuilder.finish()
+ mapping2 = indirectStore.optimize()
+ mapping = [mapping2[mapping[g]] for g in glyphOrder]
+ advanceMapping = builder.buildVarIdxMap(mapping, glyphOrder)
+
+ use_direct = False
+ if directStore:
+ # Compile both, see which is more compact
+
+ writer = OTTableWriter()
+ directStore.compile(writer, font)
+ directSize = len(writer.getAllData())
+
+ writer = OTTableWriter()
+ indirectStore.compile(writer, font)
+ advanceMapping.compile(writer, font)
+ indirectSize = len(writer.getAllData())
+
+ use_direct = directSize < indirectSize
# Done; put it all together.
assert "HVAR" not in font
@@ -482,12 +500,11 @@ def _add_HVAR(font, model, master_ttfs, axisTags):
hvar.VarStore = indirectStore
hvar.AdvWidthMap = advanceMapping
-def _add_MVAR(font, model, master_ttfs, axisTags):
+def _add_MVAR(font, masterModel, master_ttfs, axisTags):
log.info("Generating MVAR")
store_builder = varStore.OnlineVarStoreBuilder(axisTags)
- store_builder.setModel(model)
records = []
lastTableTag = None
@@ -497,17 +514,20 @@ def _add_MVAR(font, model, master_ttfs, axisTags):
if tableTag != lastTableTag:
tables = fontTable = None
if tableTag in font:
- # TODO Check all masters have same table set?
fontTable = font[tableTag]
- tables = [master[tableTag] for master in master_ttfs]
+ tables = [master[tableTag] if tableTag in master else None
+ for master in master_ttfs]
lastTableTag = tableTag
if tables is None:
continue
# TODO support gasp entries
+ model, tables = masterModel.getSubModel(tables)
+ store_builder.setModel(model)
+
master_values = [getattr(table, itemName) for table in tables]
- if _all_equal(master_values):
+ if models.allEqual(master_values):
base, varIdx = master_values[0], None
else:
base, varIdx = store_builder.storeMasters(master_values)
@@ -545,9 +565,7 @@ def _merge_OTL(font, model, master_fonts, axisTags):
log.info("Merging OpenType Layout tables")
merger = VariationMerger(model, axisTags, font)
- merger.mergeTables(font, master_fonts, ['GPOS'])
- # TODO Merge GSUB
- # TODO Merge GDEF itself!
+ merger.mergeTables(font, master_fonts, ['GSUB', 'GDEF', 'GPOS'])
store = merger.store_builder.finish()
if not store.VarData:
return
@@ -587,8 +605,14 @@ def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules):
space = {}
for condition in conditions:
axis_name = condition["name"]
- minimum = normalize(axis_name, condition["minimum"])
- maximum = normalize(axis_name, condition["maximum"])
+ if condition["minimum"] is not None:
+ minimum = normalize(axis_name, condition["minimum"])
+ else:
+ minimum = -1.0
+ if condition["maximum"] is not None:
+ maximum = normalize(axis_name, condition["maximum"])
+ else:
+ maximum = 1.0
tag = axis_tags[axis_name]
space[tag] = (minimum, maximum)
region.append(space)
@@ -614,9 +638,24 @@ _DesignSpaceData = namedtuple(
)
-def load_designspace(designspace_filename):
+def _add_CFF2(varFont, model, master_fonts):
+ from .cff import (convertCFFtoCFF2, addCFFVarStore, merge_region_fonts)
+ glyphOrder = varFont.getGlyphOrder()
+ convertCFFtoCFF2(varFont)
+ ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping)
+ # re-ordering the master list simplifies building the CFF2 data item lists.
+ addCFFVarStore(varFont, model) # Add VarStore to the CFF2 font.
+ merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
+
+
+def load_designspace(designspace):
+ # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
+ # never a file path, as that's already handled by caller
+ if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
+ ds = designspace
+ else: # Assume a file path
+ ds = DesignSpaceDocument.fromfile(designspace)
- ds = DesignSpaceDocument.fromfile(designspace_filename)
masters = ds.sources
if not masters:
raise VarLibError("no sources found in .designspace")
@@ -698,7 +737,7 @@ def load_designspace(designspace_filename):
)
-def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=True):
+def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
"""
Build variation font from a designspace file.
@@ -706,16 +745,27 @@ def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=T
filename as found in designspace file and map it to master font
binary as to be opened (eg. .ttf or .otf).
"""
+ if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
+ pass
+ else: # Assume a file path
+ designspace = DesignSpaceDocument.fromfile(designspace)
- ds = load_designspace(designspace_filename)
-
+ ds = load_designspace(designspace)
log.info("Building variable font")
+
log.info("Loading master fonts")
- basedir = os.path.dirname(designspace_filename)
- master_ttfs = [master_finder(os.path.join(basedir, m.filename)) for m in ds.masters]
- master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
- # Reload base font as target font
- vf = TTFont(master_ttfs[ds.base_idx])
+ master_fonts = load_masters(designspace, master_finder)
+
+ # TODO: 'master_ttfs' is unused except for return value, remove later
+ master_ttfs = []
+ for master in master_fonts:
+ try:
+ master_ttfs.append(master.reader.file.name)
+ except AttributeError:
+ master_ttfs.append(None) # in-memory fonts have no path
+
+ # Copy the base master to work from it
+ vf = deepcopy(master_fonts[ds.base_idx])
# TODO append masters as named-instances as well; needs .designspace change.
fvar = _add_fvar(vf, ds.axes, ds.instances)
@@ -748,14 +798,65 @@ def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=T
_merge_TTHinting(vf, model, master_fonts)
if 'GSUB' not in exclude and ds.rules:
_add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules)
+ if 'CFF2' not in exclude and 'CFF ' in vf:
+ _add_CFF2(vf, model, master_fonts)
for tag in exclude:
if tag in vf:
del vf[tag]
+ # TODO: Only return vf for 4.0+, the rest is unused.
return vf, model, master_ttfs
+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).
+
+ Return list of master TTFont objects in the same order they are listed in the
+ DesignSpaceDocument.
+ """
+ master_fonts = []
+
+ for master in designspace.sources:
+ # 1. If the caller already supplies a TTFont for a source, just take it.
+ if master.font:
+ font = master.font
+ master_fonts.append(font)
+ else:
+ # If a SourceDescriptor has a layer name, demand that the compiled TTFont
+ # be supplied by the caller. This spares us from modifying MasterFinder.
+ if master.layerName:
+ raise AttributeError(
+ "Designspace source '%s' specified a layer name but lacks the "
+ "required TTFont object in the 'font' attribute."
+ % (master.name or "<Unknown>")
+ )
+ else:
+ if master.path is None:
+ raise AttributeError(
+ "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)
+ master_fonts.append(font)
+
+ return master_fonts
+
+
class MasterFinder(object):
def __init__(self, template):
@@ -828,7 +929,7 @@ def main(args=None):
if outfile is None:
outfile = os.path.splitext(designspace_filename)[0] + '-VF.ttf'
- vf, model, master_ttfs = build(
+ vf, _, _ = build(
designspace_filename,
finder,
exclude=options.exclude,
diff --git a/Lib/fontTools/varLib/builder.py b/Lib/fontTools/varLib/builder.py
index 90e33375..e923b800 100644
--- a/Lib/fontTools/varLib/builder.py
+++ b/Lib/fontTools/varLib/builder.py
@@ -39,7 +39,7 @@ def _reorderItem(lst, narrows, zeroes):
out.append(lst[i])
return out
-def VarData_CalculateNumShorts(self, optimize=True):
+def VarData_calculateNumShorts(self, optimize=False):
count = self.VarRegionCount
items = self.Item
narrows = set(range(count))
@@ -55,14 +55,29 @@ def VarData_CalculateNumShorts(self, optimize=True):
# Reorder columns such that all SHORT columns come before UINT8
self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows, zeroes)
self.VarRegionCount = len(self.VarRegionIndex)
- for i in range(self.ItemCount):
+ for i in range(len(items)):
items[i] = _reorderItem(items[i], narrows, zeroes)
self.NumShorts = count - len(narrows)
else:
wides = set(range(count)) - narrows
self.NumShorts = 1+max(wides) if wides else 0
+ self.VarRegionCount = len(self.VarRegionIndex)
return self
+ot.VarData.calculateNumShorts = VarData_calculateNumShorts
+
+def VarData_CalculateNumShorts(self, optimize=True):
+ """Deprecated name for VarData_calculateNumShorts() which
+ defaults to optimize=True. Use varData.calculateNumShorts()
+ or varData.optimize()."""
+ return VarData_calculateNumShorts(self, optimize=optimize)
+
+def VarData_optimize(self):
+ return VarData_calculateNumShorts(self, optimize=True)
+
+ot.VarData.optimize = VarData_optimize
+
+
def buildVarData(varRegionIndices, items, optimize=True):
self = ot.VarData()
self.VarRegionIndex = list(varRegionIndices)
@@ -73,7 +88,7 @@ def buildVarData(varRegionIndices, items, optimize=True):
assert len(item) == regionCount
records.append(list(item))
self.ItemCount = len(self.Item)
- VarData_CalculateNumShorts(self, optimize=optimize)
+ self.calculateNumShorts(optimize=optimize)
return self
diff --git a/Lib/fontTools/varLib/cff.py b/Lib/fontTools/varLib/cff.py
new file mode 100644
index 00000000..a000dd48
--- /dev/null
+++ b/Lib/fontTools/varLib/cff.py
@@ -0,0 +1,502 @@
+import os
+from fontTools.misc.py23 import BytesIO
+from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor
+from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round
+from fontTools.cffLib import (
+ maxStackLimit,
+ TopDictIndex,
+ buildOrder,
+ topDictOperators,
+ topDictOperators2,
+ privateDictOperators,
+ privateDictOperators2,
+ FDArrayIndex,
+ FontDict,
+ VarStoreData
+)
+from fontTools.cffLib.specializer import (commandsToProgram, specializeCommands)
+from fontTools.ttLib import newTable
+from fontTools import varLib
+from fontTools.varLib.models import allEqual
+
+
+def addCFFVarStore(varFont, varModel):
+ supports = varModel.supports[1:]
+ fvarTable = varFont['fvar']
+ axisKeys = [axis.axisTag for axis in fvarTable.axes]
+ varTupleList = varLib.builder.buildVarRegionList(supports, axisKeys)
+ varTupleIndexes = list(range(len(supports)))
+ varDeltasCFFV = varLib.builder.buildVarData(varTupleIndexes, None, False)
+ varStoreCFFV = varLib.builder.buildVarStore(varTupleList, [varDeltasCFFV])
+
+ topDict = varFont['CFF2'].cff.topDictIndex[0]
+ topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV)
+
+
+def lib_convertCFFToCFF2(cff, otFont):
+ # This assumes a decompiled CFF table.
+ cff2GetGlyphOrder = cff.otFont.getGlyphOrder
+ topDictData = TopDictIndex(None, cff2GetGlyphOrder, None)
+ topDictData.items = cff.topDictIndex.items
+ cff.topDictIndex = topDictData
+ topDict = topDictData[0]
+ if hasattr(topDict, 'Private'):
+ privateDict = topDict.Private
+ else:
+ privateDict = None
+ opOrder = buildOrder(topDictOperators2)
+ topDict.order = opOrder
+ topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
+ if not hasattr(topDict, "FDArray"):
+ fdArray = topDict.FDArray = FDArrayIndex()
+ fdArray.strings = None
+ fdArray.GlobalSubrs = topDict.GlobalSubrs
+ topDict.GlobalSubrs.fdArray = fdArray
+ charStrings = topDict.CharStrings
+ if charStrings.charStringsAreIndexed:
+ charStrings.charStringsIndex.fdArray = fdArray
+ else:
+ charStrings.fdArray = fdArray
+ fontDict = FontDict()
+ fontDict.setCFF2(True)
+ fdArray.append(fontDict)
+ fontDict.Private = privateDict
+ privateOpOrder = buildOrder(privateDictOperators2)
+ for entry in privateDictOperators:
+ key = entry[1]
+ if key not in privateOpOrder:
+ if key in privateDict.rawDict:
+ # print "Removing private dict", key
+ del privateDict.rawDict[key]
+ if hasattr(privateDict, key):
+ delattr(privateDict, key)
+ # print "Removing privateDict attr", key
+ else:
+ # clean up the PrivateDicts in the fdArray
+ fdArray = topDict.FDArray
+ privateOpOrder = buildOrder(privateDictOperators2)
+ for fontDict in fdArray:
+ fontDict.setCFF2(True)
+ for key in list(fontDict.rawDict.keys()):
+ if key not in fontDict.order:
+ del fontDict.rawDict[key]
+ if hasattr(fontDict, key):
+ delattr(fontDict, key)
+
+ privateDict = fontDict.Private
+ for entry in privateDictOperators:
+ key = entry[1]
+ if key not in privateOpOrder:
+ if key in privateDict.rawDict:
+ # print "Removing private dict", key
+ del privateDict.rawDict[key]
+ if hasattr(privateDict, key):
+ delattr(privateDict, key)
+ # print "Removing privateDict attr", key
+ # Now delete up the decrecated topDict operators from CFF 1.0
+ for entry in topDictOperators:
+ key = entry[1]
+ if key not in opOrder:
+ if key in topDict.rawDict:
+ del topDict.rawDict[key]
+ if hasattr(topDict, key):
+ delattr(topDict, key)
+
+ # At this point, the Subrs and Charstrings are all still T2Charstring class
+ # easiest to fix this by compiling, then decompiling again
+ cff.major = 2
+ file = BytesIO()
+ cff.compile(file, otFont, isCFF2=True)
+ file.seek(0)
+ cff.decompile(file, otFont, isCFF2=True)
+
+
+def convertCFFtoCFF2(varFont):
+ # Convert base font to a single master CFF2 font.
+ cffTable = varFont['CFF ']
+ lib_convertCFFToCFF2(cffTable.cff, varFont)
+ newCFF2 = newTable("CFF2")
+ newCFF2.cff = cffTable.cff
+ varFont['CFF2'] = newCFF2
+ del varFont['CFF ']
+
+
+class MergeDictError(TypeError):
+ def __init__(self, key, value, values):
+ error_msg = ["For the Private Dict key '{}', ".format(key),
+ "the default font value list:",
+ "\t{}".format(value),
+ "had a different number of values than a region font:"]
+ error_msg += ["\t{}".format(region_value) for region_value in values]
+ error_msg = os.linesep.join(error_msg)
+
+
+def conv_to_int(num):
+ if num % 1 == 0:
+ return int(num)
+ return num
+
+
+pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues",
+ "FamilyOtherBlues", "BlueScale", "BlueShift",
+ "BlueFuzz", "StdHW", "StdVW", "StemSnapH",
+ "StemSnapV")
+
+
+def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model):
+ if hasattr(region_top_dicts[0], 'FDArray'):
+ regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts]
+ else:
+ regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts]
+ for fd_index, font_dict in enumerate(topDict.FDArray):
+ private_dict = font_dict.Private
+ pds = [private_dict] + [
+ regionFDArray[fd_index].Private for regionFDArray in regionFDArrays
+ ]
+ for key, value in private_dict.rawDict.items():
+ if key not in pd_blend_fields:
+ continue
+ if isinstance(value, list):
+ try:
+ values = [pd.rawDict[key] for pd in pds]
+ except KeyError:
+ del private_dict.rawDict[key]
+ print(
+ b"Warning: {key} in default font Private dict is "
+ b"missing from another font, and was "
+ b"discarded.".format(key=key))
+ continue
+ try:
+ values = zip(*values)
+ except IndexError:
+ raise MergeDictError(key, value, values)
+ """
+ Row 0 contains the first value from each master.
+ Convert each row from absolute values to relative
+ values from the previous row.
+ e.g for three masters, a list of values was:
+ master 0 OtherBlues = [-217,-205]
+ master 1 OtherBlues = [-234,-222]
+ master 1 OtherBlues = [-188,-176]
+ The call to zip() converts this to:
+ [(-217, -234, -188), (-205, -222, -176)]
+ and is converted finally to:
+ OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]]
+ """
+ dataList = []
+ prev_val_list = [0] * num_masters
+ any_points_differ = False
+ for val_list in values:
+ rel_list = [(val - prev_val_list[i]) for (
+ i, val) in enumerate(val_list)]
+ if (not any_points_differ) and not allEqual(rel_list):
+ any_points_differ = True
+ prev_val_list = val_list
+ deltas = var_model.getDeltas(rel_list)
+ # Convert numbers with no decimal part to an int.
+ deltas = [conv_to_int(delta) for delta in deltas]
+ # For PrivateDict BlueValues, the default font
+ # values are absolute, not relative to the prior value.
+ deltas[0] = val_list[0]
+ dataList.append(deltas)
+ # If there are no blend values,then
+ # we can collapse the blend lists.
+ if not any_points_differ:
+ dataList = [data[0] for data in dataList]
+ else:
+ values = [pd.rawDict[key] for pd in pds]
+ if not allEqual(values):
+ dataList = var_model.getDeltas(values)
+ else:
+ dataList = values[0]
+ private_dict.rawDict[key] = dataList
+
+
+def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder):
+ topDict = varFont['CFF2'].cff.topDictIndex[0]
+ default_charstrings = topDict.CharStrings
+ region_fonts = ordered_fonts_list[1:]
+ region_top_dicts = [
+ ttFont['CFF '].cff.topDictIndex[0] for ttFont in region_fonts
+ ]
+ num_masters = len(model.mapping)
+ merge_PrivateDicts(topDict, region_top_dicts, num_masters, model)
+ merge_charstrings(default_charstrings,
+ glyphOrder,
+ num_masters,
+ region_top_dicts, model)
+
+
+def merge_charstrings(default_charstrings,
+ glyphOrder,
+ num_masters,
+ region_top_dicts,
+ var_model):
+ for gname in glyphOrder:
+ default_charstring = default_charstrings[gname]
+ var_pen = CFF2CharStringMergePen([], gname, num_masters, 0)
+ default_charstring.outlineExtractor = CFFToCFF2OutlineExtractor
+ default_charstring.draw(var_pen)
+ for region_idx, region_td in enumerate(region_top_dicts, start=1):
+ region_charstrings = region_td.CharStrings
+ region_charstring = region_charstrings[gname]
+ var_pen.restart(region_idx)
+ region_charstring.draw(var_pen)
+ new_charstring = var_pen.getCharString(
+ private=default_charstring.private,
+ globalSubrs=default_charstring.globalSubrs,
+ var_model=var_model, optimize=True)
+ default_charstrings[gname] = new_charstring
+
+
+class MergeTypeError(TypeError):
+ def __init__(self, point_type, pt_index, m_index, default_type, glyphName):
+ self.error_msg = [
+ "In glyph '{gname}' "
+ "'{point_type}' at point index {pt_index} in master "
+ "index {m_index} differs from the default font point "
+ "type '{default_type}'"
+ "".format(gname=glyphName,
+ point_type=point_type, pt_index=pt_index,
+ m_index=m_index, default_type=default_type)
+ ][0]
+ super(MergeTypeError, self).__init__(self.error_msg)
+
+
+def makeRoundNumberFunc(tolerance):
+ if tolerance < 0:
+ raise ValueError("Rounding tolerance must be positive")
+
+ def roundNumber(val):
+ return t2c_round(val, tolerance)
+
+ return roundNumber
+
+
+class CFFToCFF2OutlineExtractor(T2OutlineExtractor):
+ """ This class is used to remove the initial width
+ from the CFF charstring without adding the width
+ to self.nominalWidthX, which is None.
+ """
+ def popallWidth(self, evenOdd=0):
+ args = self.popall()
+ if not self.gotWidth:
+ if evenOdd ^ (len(args) % 2):
+ args = args[1:]
+ self.width = self.defaultWidthX
+ self.gotWidth = 1
+ return args
+
+
+class CFF2CharStringMergePen(T2CharStringPen):
+ """Pen to merge Type 2 CharStrings.
+ """
+ def __init__(self, default_commands,
+ glyphName, num_masters, master_idx, roundTolerance=0.5):
+ super(
+ CFF2CharStringMergePen,
+ self).__init__(width=None,
+ glyphSet=None, CFF2=True,
+ roundTolerance=roundTolerance)
+ self.pt_index = 0
+ self._commands = default_commands
+ self.m_index = master_idx
+ self.num_masters = num_masters
+ self.prev_move_idx = 0
+ self.glyphName = glyphName
+ self.roundNumber = makeRoundNumberFunc(roundTolerance)
+
+ def _p(self, pt):
+ """ Unlike T2CharstringPen, this class stores absolute values.
+ This is to allow the logic in check_and_fix_closepath() to work,
+ where the current or previous absolute point has to be compared to
+ the path start-point.
+ """
+ self._p0 = pt
+ return list(self._p0)
+
+ def add_point(self, point_type, pt_coords):
+ if self.m_index == 0:
+ self._commands.append([point_type, [pt_coords]])
+ else:
+ cmd = self._commands[self.pt_index]
+ if cmd[0] != point_type:
+ # Fix some issues that show up in some
+ # CFF workflows, even when fonts are
+ # topologically merge compatible.
+ success, pt_coords = self.check_and_fix_flat_curve(
+ cmd, point_type, pt_coords)
+ if not success:
+ success = self.check_and_fix_closepath(
+ cmd, point_type, pt_coords)
+ if success:
+ # We may have incremented self.pt_index
+ cmd = self._commands[self.pt_index]
+ if cmd[0] != point_type:
+ success = False
+ if not success:
+ raise MergeTypeError(point_type,
+ self.pt_index, len(cmd[1]),
+ cmd[0], self.glyphName)
+ cmd[1].append(pt_coords)
+ self.pt_index += 1
+
+ def _moveTo(self, pt):
+ pt_coords = self._p(pt)
+ self.add_point('rmoveto', pt_coords)
+ # I set prev_move_idx here because add_point()
+ # can change self.pt_index.
+ self.prev_move_idx = self.pt_index - 1
+
+ def _lineTo(self, pt):
+ pt_coords = self._p(pt)
+ self.add_point('rlineto', pt_coords)
+
+ def _curveToOne(self, pt1, pt2, pt3):
+ _p = self._p
+ pt_coords = _p(pt1)+_p(pt2)+_p(pt3)
+ self.add_point('rrcurveto', pt_coords)
+
+ def _closePath(self):
+ pass
+
+ def _endPath(self):
+ pass
+
+ def restart(self, region_idx):
+ self.pt_index = 0
+ self.m_index = region_idx
+ self._p0 = (0, 0)
+
+ def getCommands(self):
+ return self._commands
+
+ def reorder_blend_args(self, commands):
+ """
+ We first re-order the master coordinate values.
+ For a moveto to lineto, the args are now arranged as:
+ [ [master_0 x,y], [master_1 x,y], [master_2 x,y] ]
+ We re-arrange this to
+ [ [master_0 x, master_1 x, master_2 x],
+ [master_0 y, master_1 y, master_2 y]
+ ]
+ We also make the value relative.
+ If the master values are all the same, we collapse the list to
+ as single value instead of a list.
+ """
+ for cmd in commands:
+ # arg[i] is the set of arguments for this operator from master i.
+ args = cmd[1]
+ m_args = zip(*args)
+ # m_args[n] is now all num_master args for the i'th argument
+ # for this operation.
+ cmd[1] = m_args
+
+ # Now convert from absolute to relative
+ x0 = [0]*self.num_masters
+ y0 = [0]*self.num_masters
+ for cmd in self._commands:
+ is_x = True
+ coords = cmd[1]
+ rel_coords = []
+ for coord in coords:
+ prev_coord = x0 if is_x else y0
+ rel_coord = [pt[0] - pt[1] for pt in zip(coord, prev_coord)]
+
+ if allEqual(rel_coord):
+ rel_coord = rel_coord[0]
+ rel_coords.append(rel_coord)
+ if is_x:
+ x0 = coord
+ else:
+ y0 = coord
+ is_x = not is_x
+ cmd[1] = rel_coords
+ return commands
+
+ @staticmethod
+ def mergeCommandsToProgram(commands, var_model, round_func):
+ """
+ Takes a commands list as returned by programToCommands() and
+ converts it back to a T2CharString or CFF2Charstring program list. I
+ need to use this rather than specialize.commandsToProgram, as the
+ commands produced by CFF2CharStringMergePen initially contains a
+ list of coordinate values, one for each master, wherever a single
+ coordinate value is expected by the regular logic. The problem with
+ doing using the specialize.py functions is that a commands list is
+ expected to be a op name with its associated argument list. For the
+ commands list here, some of the arguments may need to be converted
+ to a new argument list and opcode.
+ This version will convert each list of master arguments to a blend
+ op and its arguments, and will also combine successive blend ops up
+ to the stack limit.
+ """
+ program = []
+ for op, args in commands:
+ num_args = len(args)
+ # some of the args may be blend lists, and some may be
+ # single coordinate values.
+ i = 0
+ stack_use = 0
+ while i < num_args:
+ arg = args[i]
+ if not isinstance(arg, list):
+ program.append(arg)
+ i += 1
+ stack_use += 1
+ else:
+ prev_stack_use = stack_use
+ """ The arg is a tuple of blend values.
+ These are each (master 0,master 1..master n)
+ Combine as many successive tuples as we can,
+ up to the max stack limit.
+ """
+ num_masters = len(arg)
+ blendlist = [arg]
+ i += 1
+ stack_use += 1 + num_masters # 1 for the num_blends arg
+ while (i < num_args) and isinstance(args[i], list):
+ blendlist.append(args[i])
+ i += 1
+ stack_use += num_masters
+ if stack_use + num_masters > maxStackLimit:
+ # if we are here, max stack is is the CFF2 max stack.
+ break
+ num_blends = len(blendlist)
+ # append the 'num_blends' default font values
+ for arg in blendlist:
+ if round_func:
+ arg[0] = round_func(arg[0])
+ program.append(arg[0])
+ for arg in blendlist:
+ # for each coordinate tuple, append the region deltas
+ if len(arg) != 3:
+ print(arg)
+ import pdb
+ pdb.set_trace()
+ deltas = var_model.getDeltas(arg)
+ if round_func:
+ deltas = [round_func(delta) for delta in deltas]
+ # First item in 'deltas' is the default master value;
+ # for CFF2 data, that has already been written.
+ program.extend(deltas[1:])
+ program.append(num_blends)
+ program.append('blend')
+ stack_use = prev_stack_use + num_blends
+ if op:
+ program.append(op)
+ return program
+
+
+ def getCharString(self, private=None, globalSubrs=None,
+ var_model=None, optimize=True):
+ commands = self._commands
+ commands = self.reorder_blend_args(commands)
+ if optimize:
+ commands = specializeCommands(commands, generalizeFirst=False,
+ maxstack=maxStackLimit)
+ program = self.mergeCommandsToProgram(commands, var_model=var_model,
+ round_func=self.roundNumber)
+ charString = T2CharString(program=program, private=private,
+ globalSubrs=globalSubrs)
+ return charString
diff --git a/Lib/fontTools/varLib/featureVars.py b/Lib/fontTools/varLib/featureVars.py
index 60336215..de2808db 100644
--- a/Lib/fontTools/varLib/featureVars.py
+++ b/Lib/fontTools/varLib/featureVars.py
@@ -4,11 +4,13 @@ https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariat
NOTE: The API is experimental and subject to change.
"""
from __future__ import print_function, absolute_import, division
-
+from fontTools.misc.py23 import *
+from fontTools.misc.dictTools import hashdict
+from fontTools.misc.intTools import popCount
from fontTools.ttLib import newTable
from fontTools.ttLib.tables import otTables as ot
from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
-import itertools
+from collections import OrderedDict
def addFeatureVariations(font, conditionalSubstitutions):
@@ -17,160 +19,236 @@ def addFeatureVariations(font, conditionalSubstitutions):
The `conditionalSubstitutions` argument is a list of (Region, Substitutions)
tuples.
- A Region is a list of Spaces. A Space is a dict mapping axisTags to
- (minValue, maxValue) tuples. Irrelevant axes may be omitted.
- A Space represents a 'rectangular' subset of an N-dimensional design space.
+ A Region is a list of Boxes. A Box is a dict mapping axisTags to
+ (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are
+ interpretted as extending to end of axis in each direction. A Box represents
+ an orthogonal 'rectangular' subset of an N-dimensional design space.
A Region represents a more complex subset of an N-dimensional design space,
- ie. the union of all the Spaces in the Region.
- For efficiency, Spaces within a Region should ideally not overlap, but
+ ie. the union of all the Boxes in the Region.
+ For efficiency, Boxes within a Region should ideally not overlap, but
functionality is not compromised if they do.
The minimum and maximum values are expressed in normalized coordinates.
A Substitution is a dict mapping source glyph names to substitute glyph names.
+
+ Example:
+
+ # >>> f = TTFont(srcPath)
+ # >>> condSubst = [
+ # ... # A list of (Region, Substitution) tuples.
+ # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
+ # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
+ # ... ]
+ # >>> addFeatureVariations(f, condSubst)
+ # >>> f.save(dstPath)
"""
- # Example:
- #
- # >>> f = TTFont(srcPath)
- # >>> condSubst = [
- # ... # A list of (Region, Substitution) tuples.
- # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
- # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
- # ... ]
- # >>> addFeatureVariations(f, condSubst)
- # >>> f.save(dstPath)
-
- # Since the FeatureVariations table will only ever match one rule at a time,
- # we will make new rules for all possible combinations of our input, so we
- # can indirectly support overlapping rules.
- explodedConditionalSubstitutions = []
- for combination in iterAllCombinations(len(conditionalSubstitutions)):
- regions = []
- lookups = []
- for index in combination:
- regions.append(conditionalSubstitutions[index][0])
- lookups.append(conditionalSubstitutions[index][1])
- if not regions:
- continue
- intersection = regions[0]
- for region in regions[1:]:
- intersection = intersectRegions(intersection, region)
- for space in intersection:
- # Remove default values, so we don't generate redundant ConditionSets
- space = cleanupSpace(space)
- if space:
- explodedConditionalSubstitutions.append((space, lookups))
-
- addFeatureVariationsRaw(font, explodedConditionalSubstitutions)
-
-
-def iterAllCombinations(numRules):
- """Given a number of rules, yield all the combinations of indices, sorted
- by decreasing length, so we get the most specialized rules first.
-
- >>> list(iterAllCombinations(0))
- []
- >>> list(iterAllCombinations(1))
- [(0,)]
- >>> list(iterAllCombinations(2))
- [(0, 1), (0,), (1,)]
- >>> list(iterAllCombinations(3))
- [(0, 1, 2), (0, 1), (0, 2), (1, 2), (0,), (1,), (2,)]
+ addFeatureVariationsRaw(font,
+ overlayFeatureVariations(conditionalSubstitutions))
+
+def overlayFeatureVariations(conditionalSubstitutions):
+ """Compute overlaps between all conditional substitutions.
+
+ The `conditionalSubstitutions` argument is a list of (Region, Substitutions)
+ tuples.
+
+ A Region is a list of Boxes. A Box is a dict mapping axisTags to
+ (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are
+ interpretted as extending to end of axis in each direction. A Box represents
+ an orthogonal 'rectangular' subset of an N-dimensional design space.
+ A Region represents a more complex subset of an N-dimensional design space,
+ ie. the union of all the Boxes in the Region.
+ For efficiency, Boxes within a Region should ideally not overlap, but
+ functionality is not compromised if they do.
+
+ The minimum and maximum values are expressed in normalized coordinates.
+
+ A Substitution is a dict mapping source glyph names to substitute glyph names.
+
+ Returns data is in similar but different format. Overlaps of distinct
+ substitution Boxes (*not* Regions) are explicitly listed as distinct rules,
+ and rules with the same Box merged. The more specific rules appear earlier
+ in the resulting list. Moreover, instead of just a dictionary of substitutions,
+ a list of dictionaries is returned for substitutions corresponding to each
+ uniq space, with each dictionary being identical to one of the input
+ substitution dictionaries. These dictionaries are not merged to allow data
+ sharing when they are converted into font tables.
+
+ Example:
+ >>> condSubst = [
+ ... # A list of (Region, Substitution) tuples.
+ ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
+ ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
+ ... ]
+ >>> from pprint import pprint
+ >>> pprint(overlayFeatureVariations(condSubst))
+ [({'wdth': (0.5, 1.0), 'wght': (0.5, 1.0)},
+ [{'dollar': 'dollar.rvrn'}, {'cent': 'cent.rvrn'}]),
+ ({'wdth': (0.5, 1.0)}, [{'cent': 'cent.rvrn'}]),
+ ({'wght': (0.5, 1.0)}, [{'dollar': 'dollar.rvrn'}])]
"""
- indices = range(numRules)
- for length in range(numRules, 0, -1):
- for combinations in itertools.combinations(indices, length):
- yield combinations
+
+ # Merge same-substitutions rules, as this creates fewer number oflookups.
+ merged = OrderedDict()
+ for value,key in conditionalSubstitutions:
+ key = hashdict(key)
+ if key in merged:
+ merged[key].extend(value)
+ else:
+ merged[key] = value
+ conditionalSubstitutions = [(v,dict(k)) for k,v in merged.items()]
+ del merged
+
+ # Merge same-region rules, as this is cheaper.
+ # Also convert boxes to hashdict()
+ #
+ # Reversing is such that earlier entries win in case of conflicting substitution
+ # rules for the same region.
+ merged = OrderedDict()
+ for key,value in reversed(conditionalSubstitutions):
+ key = tuple(sorted(hashdict(cleanupBox(k)) for k in key))
+ if key in merged:
+ merged[key].update(value)
+ else:
+ merged[key] = dict(value)
+ conditionalSubstitutions = list(reversed(merged.items()))
+ del merged
+
+ # Overlay
+ #
+ # Rank is the bit-set of the index of all contributing layers.
+ initMapInit = ((hashdict(),0),) # Initializer representing the entire space
+ boxMap = OrderedDict(initMapInit) # Map from Box to Rank
+ for i,(currRegion,_) in enumerate(conditionalSubstitutions):
+ newMap = OrderedDict(initMapInit)
+ currRank = 1<<i
+ for box,rank in boxMap.items():
+ for currBox in currRegion:
+ intersection, remainder = overlayBox(currBox, box)
+ if intersection is not None:
+ intersection = hashdict(intersection)
+ newMap[intersection] = newMap.get(intersection, 0) | rank|currRank
+ if remainder is not None:
+ remainder = hashdict(remainder)
+ newMap[remainder] = newMap.get(remainder, 0) | rank
+ boxMap = newMap
+ del boxMap[hashdict()]
+
+ # Generate output
+ items = []
+ for box,rank in sorted(boxMap.items(),
+ key=(lambda BoxAndRank: -popCount(BoxAndRank[1]))):
+ substsList = []
+ i = 0
+ while rank:
+ if rank & 1:
+ substsList.append(conditionalSubstitutions[i][1])
+ rank >>= 1
+ i += 1
+ items.append((dict(box),substsList))
+ return items
-#
-# Region and Space support
#
# Terminology:
#
-# A 'Space' is a dict representing a "rectangular" bit of N-dimensional space.
+# A 'Box' is a dict representing an orthogonal "rectangular" bit of N-dimensional space.
# The keys in the dict are axis tags, the values are (minValue, maxValue) tuples.
# Missing dimensions (keys) are substituted by the default min and max values
# from the corresponding axes.
#
-# A 'Region' is a list of Space dicts, representing the union of the Spaces,
-# therefore representing a more complex subset of design space.
-#
-def intersectRegions(region1, region2):
- """Return the region intersecting `region1` and `region2`.
-
- >>> intersectRegions([], [])
- []
- >>> intersectRegions([{'wdth': (0.0, 1.0)}], [])
- []
- >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-1.0, 0.0)}]
- >>> expected == intersectRegions([{'wdth': (0.0, 1.0)}], [{'wght': (-1.0, 0.0)}])
- True
- >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.0)}]
- >>> expected == intersectRegions([{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], [{'wght': (-1.0, 0.0)}])
- True
- >>> intersectRegions(
- ... [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}],
- ... [{'wdth': (-1.0, 0.0), 'wght': (-1.0, 0.0)}])
- []
+def overlayBox(top, bot):
+ """Overlays `top` box on top of `bot` box.
+ Returns two items:
+ - Box for intersection of `top` and `bot`, or None if they don't intersect.
+ - Box for remainder of `bot`. Remainder box might not be exact (since the
+ remainder might not be a simple box), but is inclusive of the exact
+ remainder.
"""
- region = []
- for space1 in region1:
- for space2 in region2:
- space = intersectSpaces(space1, space2)
- if space is not None:
- region.append(space)
- return region
-
-def intersectSpaces(space1, space2):
- """Return the space intersected by `space1` and `space2`, or None if there
- is no intersection.
-
- >>> intersectSpaces({}, {})
- {}
- >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {})
- {'wdth': (-0.5, 0.5)}
- >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {'wdth': (0.0, 1.0)})
- {'wdth': (0.0, 0.5)}
- >>> expected = {'wdth': (0.0, 0.5), 'wght': (0.25, 0.5)}
- >>> expected == intersectSpaces({'wdth': (-0.5, 0.5), 'wght': (0.0, 0.5)}, {'wdth': (0.0, 1.0), 'wght': (0.25, 0.75)})
- True
- >>> expected = {'wdth': (-0.5, 0.5), 'wght': (0.0, 1.0)}
- >>> expected == intersectSpaces({'wdth': (-0.5, 0.5)}, {'wght': (0.0, 1.0)})
- True
- >>> intersectSpaces({'wdth': (-0.5, 0)}, {'wdth': (0.1, 0.5)})
-
- """
- space = {}
- space.update(space1)
- space.update(space2)
- for axisTag in set(space1) & set(space2):
- min1, max1 = space1[axisTag]
- min2, max2 = space2[axisTag]
+ # Intersection
+ intersection = {}
+ intersection.update(top)
+ intersection.update(bot)
+ for axisTag in set(top) & set(bot):
+ min1, max1 = top[axisTag]
+ min2, max2 = bot[axisTag]
minimum = max(min1, min2)
maximum = min(max1, max2)
if not minimum < maximum:
- return None
- space[axisTag] = minimum, maximum
- return space
-
-
-def cleanupSpace(space):
- """Return a sparse copy of `space`, without redundant (default) values.
+ return None, bot # Do not intersect
+ intersection[axisTag] = minimum,maximum
- >>> cleanupSpace({})
+ # Remainder
+ #
+ # Remainder is empty if bot's each axis range lies within that of intersection.
+ #
+ # Remainder is shrank if bot's each, except for exactly one, axis range lies
+ # within that of intersection, and that one axis, it spills out of the
+ # intersection only on one side.
+ #
+ # Bot is returned in full as remainder otherwise, as true remainder is not
+ # representable as a single box.
+
+ remainder = dict(bot)
+ exactlyOne = False
+ fullyInside = False
+ for axisTag in bot:
+ if axisTag not in intersection:
+ fullyInside = False
+ continue # Axis range lies fully within
+ min1, max1 = intersection[axisTag]
+ min2, max2 = bot[axisTag]
+ if min1 <= min2 and max2 <= max1:
+ continue # Axis range lies fully within
+
+ # Bot's range doesn't fully lie within that of top's for this axis.
+ # We know they intersect, so it cannot lie fully without either; so they
+ # overlap.
+
+ # If we have had an overlapping axis before, remainder is not
+ # representable as a box, so return full bottom and go home.
+ if exactlyOne:
+ return intersection, bot
+ exactlyOne = True
+ fullyInside = False
+
+ # Otherwise, cut remainder on this axis and continue.
+ if min1 <= min2:
+ # Right side survives.
+ minimum = max(max1, min2)
+ maximum = max2
+ elif max2 <= max1:
+ # Left side survives.
+ minimum = min2
+ maximum = min(min1, max2)
+ else:
+ # Remainder leaks out from both sides. Can't cut either.
+ return intersection, bot
+
+ remainder[axisTag] = minimum,maximum
+
+ if fullyInside:
+ # bot is fully within intersection. Remainder is empty.
+ return intersection, None
+
+ return intersection, remainder
+
+def cleanupBox(box):
+ """Return a sparse copy of `box`, without redundant (default) values.
+
+ >>> cleanupBox({})
{}
- >>> cleanupSpace({'wdth': (0.0, 1.0)})
+ >>> cleanupBox({'wdth': (0.0, 1.0)})
{'wdth': (0.0, 1.0)}
- >>> cleanupSpace({'wdth': (-1.0, 1.0)})
+ >>> cleanupBox({'wdth': (-1.0, 1.0)})
{}
"""
- return {tag: limit for tag, limit in space.items() if limit != (-1.0, 1.0)}
+ return {tag: limit for tag, limit in box.items() if limit != (-1.0, 1.0)}
#
@@ -210,7 +288,8 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions):
rvrnFeatureIndex = gsub.FeatureList.FeatureRecord.index(rvrnFeature)
for scriptRecord in gsub.ScriptList.ScriptRecord:
- for langSys in [scriptRecord.Script.DefaultLangSys] + scriptRecord.Script.LangSysRecord:
+ langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord]
+ for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems:
langSys.FeatureIndex.append(rvrnFeatureIndex)
# setup lookups
@@ -327,7 +406,7 @@ def buildFeatureVariationRecord(conditionTable, substitutionRecords):
fvr.ConditionSet = ot.ConditionSet()
fvr.ConditionSet.ConditionTable = conditionTable
fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution()
- fvr.FeatureTableSubstitution.Version = 0x00010001
+ fvr.FeatureTableSubstitution.Version = 0x00010000
fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords
return fvr
@@ -388,5 +467,5 @@ def _remapLangSys(langSys, featureRemap):
if __name__ == "__main__":
- import doctest
- doctest.testmod()
+ import doctest, sys
+ sys.exit(doctest.testmod().failed)
diff --git a/Lib/fontTools/varLib/interpolate_layout.py b/Lib/fontTools/varLib/interpolate_layout.py
index 1ce3fafb..f252149a 100644
--- a/Lib/fontTools/varLib/interpolate_layout.py
+++ b/Lib/fontTools/varLib/interpolate_layout.py
@@ -4,16 +4,17 @@ Interpolate OpenType Layout tables (GDEF / GPOS / GSUB).
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from fontTools.ttLib import TTFont
-from fontTools.varLib import models, VarLibError, load_designspace
+from fontTools.varLib import models, VarLibError, load_designspace, load_masters
from fontTools.varLib.merger import InstancerMerger
import os.path
import logging
+from copy import deepcopy
from pprint import pformat
log = logging.getLogger("fontTools.varLib.interpolate_layout")
-def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapped=False):
+def interpolate_layout(designspace, loc, master_finder=lambda s:s, mapped=False):
"""
Interpolate GPOS from a designspace file and location.
@@ -26,18 +27,18 @@ def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapp
it is assumed that location is in designspace's internal space and
no mapping is performed.
"""
- ds = load_designspace(designspace_filename)
+ if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
+ pass
+ else: # Assume a file path
+ from fontTools.designspaceLib import DesignSpaceDocument
+ designspace = DesignSpaceDocument.fromfile(designspace)
+ ds = load_designspace(designspace)
log.info("Building interpolated font")
- log.info("Loading master fonts")
- basedir = os.path.dirname(designspace_filename)
- master_ttfs = [
- master_finder(os.path.join(basedir, m.filename)) for m in ds.masters
- ]
- master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
- #font = master_fonts[ds.base_idx]
- font = TTFont(master_ttfs[ds.base_idx])
+ log.info("Loading master fonts")
+ master_fonts = load_masters(designspace, master_finder)
+ font = deepcopy(master_fonts[ds.base_idx])
log.info("Location: %s", pformat(loc))
if not mapped:
@@ -53,6 +54,7 @@ def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapp
merger = InstancerMerger(font, model, loc)
log.info("Building interpolated tables")
+ # TODO GSUB/GDEF
merger.mergeTables(font, master_fonts, ['GPOS'])
return font
diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py
index aaee3118..688790c5 100644
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -8,7 +8,8 @@ from fontTools.misc import classifyTools
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables import otBase as otBase
from fontTools.ttLib.tables.DefaultTable import DefaultTable
-from fontTools.varLib import builder, varStore
+from fontTools.varLib import builder, models, varStore
+from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo
from fontTools.varLib.varStore import VarStoreInstancer
from functools import reduce
@@ -75,8 +76,7 @@ class Merger(object):
raise
def mergeLists(self, out, lst):
- count = len(out)
- assert all(count == len(v) for v in lst), (count, [len(v) for v in lst])
+ assert allEqualTo(out, lst, len), (len(out), [len(v) for v in lst])
for i,(value,values) in enumerate(zip(out, zip(*lst))):
try:
self.mergeThings(value, values)
@@ -85,9 +85,8 @@ class Merger(object):
raise
def mergeThings(self, out, lst):
- clazz = type(out)
try:
- assert all(type(item) == clazz for item in lst), (out, lst)
+ assert allEqualTo(out, lst, type), (out, lst)
mergerFunc = self.mergersFor(out).get(None, None)
if mergerFunc is not None:
mergerFunc(self, out, lst)
@@ -96,16 +95,17 @@ class Merger(object):
elif isinstance(out, list):
self.mergeLists(out, lst)
else:
- assert all(out == v for v in lst), (out, lst)
+ assert allEqualTo(out, lst), (out, lst)
except Exception as e:
- e.args = e.args + (clazz.__name__,)
+ e.args = e.args + (type(out).__name__,)
raise
- def mergeTables(self, font, master_ttfs, tables):
+ def mergeTables(self, font, master_ttfs, tableTags):
- for tag in tables:
+ for tag in tableTags:
if tag not in font: continue
- self.mergeThings(font[tag], [m[tag] for m in master_ttfs])
+ self.mergeThings(font[tag], [m[tag] if tag in m else None
+ for m in master_ttfs])
#
# Aligning merger
@@ -113,6 +113,27 @@ class Merger(object):
class AligningMerger(Merger):
pass
+@AligningMerger.merger(ot.GDEF, "GlyphClassDef")
+def merge(merger, self, lst):
+ if self is None:
+ assert allNone(lst), (lst)
+ return
+
+ self.classDefs = {}
+ # We only care about the .classDefs
+ self = self.classDefs
+ lst = [l.classDefs for l in lst]
+
+ allKeys = set()
+ allKeys.update(*[l.keys() for l in lst])
+ for k in allKeys:
+ allValues = nonNone(l.get(k) for l in lst)
+ assert allEqual(allValues), allValues
+ if not allValues:
+ self[k] = None
+ else:
+ self[k] = allValues[0]
+
def _SinglePosUpgradeToFormat2(self):
if self.Format == 2: return self
@@ -201,7 +222,8 @@ def merge(merger, self, lst):
assert len(lst) == 1 or (valueFormat & ~0xF == 0), valueFormat
# If all have same coverage table and all are format 1,
- if all(v.Format == 1 for v in lst) and all(self.Coverage.glyphs == v.Coverage.glyphs for v in lst):
+ coverageGlyphs = self.Coverage.glyphs
+ if all(v.Format == 1 for v in lst) and all(coverageGlyphs == v.Coverage.glyphs for v in lst):
self.Value = otBase.ValueRecord(valueFormat)
merger.mergeThings(self.Value, [v.Value for v in lst])
self.ValueFormat = self.Value.getFormat()
@@ -276,7 +298,7 @@ def merge(merger, self, lst):
merger.mergeLists(self.PairValueRecord, padded)
def _PairPosFormat1_merge(self, lst, merger):
- assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools."
+ assert allEqual([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools."
# Merge everything else; makes sure Format is the same.
merger.mergeObjects(self, lst,
@@ -330,19 +352,22 @@ def _ClassDef_invert(self, allGlyphs=None):
return ret
-def _ClassDef_merge_classify(lst, allGlyphs=None):
+def _ClassDef_merge_classify(lst, allGlyphses=None):
self = ot.ClassDef()
self.classDefs = classDefs = {}
+ allGlyphsesWasNone = allGlyphses is None
+ if allGlyphsesWasNone:
+ allGlyphses = [None] * len(lst)
classifier = classifyTools.Classifier()
- for l in lst:
- sets = _ClassDef_invert(l, allGlyphs=allGlyphs)
+ for classDef,allGlyphs in zip(lst, allGlyphses):
+ sets = _ClassDef_invert(classDef, allGlyphs)
if allGlyphs is None:
sets = sets[1:]
classifier.update(sets)
classes = classifier.getClasses()
- if allGlyphs is None:
+ if allGlyphsesWasNone:
classes.insert(0, set())
for i,classSet in enumerate(classes):
@@ -353,6 +378,9 @@ def _ClassDef_merge_classify(lst, allGlyphs=None):
return self, classes
+# It's stupid that we need to do this here. Just need to, to match test
+# expecatation results, since ttx prints out format of ClassDef (and Coverage)
+# even though it should not.
def _ClassDef_calculate_Format(self, font):
fmt = 2
ranges = self._getClassRanges(font)
@@ -370,7 +398,7 @@ def _PairPosFormat2_align_matrices(self, lst, font, transparent=False):
matrices = [l.Class1Record for l in lst]
# Align first classes
- self.ClassDef1, classes = _ClassDef_merge_classify([l.ClassDef1 for l in lst], allGlyphs=set(self.Coverage.glyphs))
+ self.ClassDef1, classes = _ClassDef_merge_classify([l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst])
_ClassDef_calculate_Format(self.ClassDef1, font)
self.Class1Count = len(classes)
new_matrices = []
@@ -382,6 +410,11 @@ def _PairPosFormat2_align_matrices(self, lst, font, transparent=False):
for classSet in classes:
exemplarGlyph = next(iter(classSet))
if exemplarGlyph not in coverage:
+ # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f,
+ # Fixes https://github.com/googlei18n/fontmake/issues/470
+ # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9
+ # when merger becomes selfless.
+ nullRow = None
if nullRow is None:
nullRow = ot.Class1Record()
class2records = nullRow.Class2Record = []
@@ -431,7 +464,7 @@ def _PairPosFormat2_align_matrices(self, lst, font, transparent=False):
return matrices
def _PairPosFormat2_merge(self, lst, merger):
- assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools."
+ assert allEqual([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools."
merger.mergeObjects(self, lst,
exclude=('Coverage',
@@ -500,6 +533,105 @@ def merge(merger, self, lst):
self.ValueFormat1 = vf1
self.ValueFormat2 = vf2
+def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'):
+ self.ClassCount = max(l.ClassCount for l in lst)
+
+ MarkCoverageGlyphs, MarkRecords = \
+ _merge_GlyphOrders(merger.font,
+ [getattr(l, Mark+'Coverage').glyphs for l in lst],
+ [getattr(l, Mark+'Array').MarkRecord for l in lst])
+ getattr(self, Mark+'Coverage').glyphs = MarkCoverageGlyphs
+
+ BaseCoverageGlyphs, BaseRecords = \
+ _merge_GlyphOrders(merger.font,
+ [getattr(l, Base+'Coverage').glyphs for l in lst],
+ [getattr(getattr(l, Base+'Array'), Base+'Record') for l in lst])
+ getattr(self, Base+'Coverage').glyphs = BaseCoverageGlyphs
+
+ # MarkArray
+ records = []
+ for g,glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)):
+ allClasses = [r.Class for r in glyphRecords if r is not None]
+
+ # TODO Right now we require that all marks have same class in
+ # all masters that cover them. This is not required.
+ #
+ # We can relax that by just requiring that all marks that have
+ # the same class in a master, have the same class in every other
+ # master. Indeed, if, say, a sparse master only covers one mark,
+ # that mark probably will get class 0, which would possibly be
+ # different from its class in other masters.
+ #
+ # We can even go further and reclassify marks to support any
+ # input. But, since, it's unlikely that two marks being both,
+ # say, "top" in one master, and one being "top" and other being
+ # "top-right" in another master, we shouldn't do that, as any
+ # failures in that case will probably signify mistakes in the
+ # input masters.
+
+ assert allEqual(allClasses), allClasses
+ if not allClasses:
+ rec = None
+ else:
+ rec = ot.MarkRecord()
+ rec.Class = allClasses[0]
+ allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords]
+ if allNone(allAnchors):
+ anchor = None
+ else:
+ anchor = ot.Anchor()
+ anchor.Format = 1
+ merger.mergeThings(anchor, allAnchors)
+ rec.MarkAnchor = anchor
+ records.append(rec)
+ array = ot.MarkArray()
+ array.MarkRecord = records
+ array.MarkCount = len(records)
+ setattr(self, Mark+"Array", array)
+
+ # BaseArray
+ records = []
+ for g,glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)):
+ if allNone(glyphRecords):
+ rec = None
+ else:
+ rec = getattr(ot, Base+'Record')()
+ anchors = []
+ setattr(rec, Base+'Anchor', anchors)
+ glyphAnchors = [[] if r is None else getattr(r, Base+'Anchor')
+ for r in glyphRecords]
+ for l in glyphAnchors:
+ l.extend([None] * (self.ClassCount - len(l)))
+ for allAnchors in zip(*glyphAnchors):
+ if allNone(allAnchors):
+ anchor = None
+ else:
+ anchor = ot.Anchor()
+ anchor.Format = 1
+ merger.mergeThings(anchor, allAnchors)
+ anchors.append(anchor)
+ records.append(rec)
+ array = getattr(ot, Base+'Array')()
+ setattr(array, Base+'Record', records)
+ setattr(array, Base+'Count', len(records))
+ setattr(self, Base+'Array', array)
+
+@AligningMerger.merger(ot.MarkBasePos)
+def merge(merger, self, lst):
+ assert allEqualTo(self.Format, (l.Format for l in lst))
+ if self.Format == 1:
+ _MarkBasePosFormat1_merge(self, lst, merger)
+ else:
+ assert False
+
+@AligningMerger.merger(ot.MarkMarkPos)
+def merge(merger, self, lst):
+ assert allEqualTo(self.Format, (l.Format for l in lst))
+ if self.Format == 1:
+ _MarkBasePosFormat1_merge(self, lst, merger, 'Mark1', 'Mark2')
+ else:
+ assert False
+
def _PairSet_flatten(lst, font):
self = ot.PairSet()
@@ -525,7 +657,7 @@ def _PairSet_flatten(lst, font):
return self
def _Lookup_PairPosFormat1_subtables_flatten(lst, font):
- assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools."
+ assert allEqual([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools."
self = ot.PairPos()
self.Format = 1
@@ -546,7 +678,7 @@ def _Lookup_PairPosFormat1_subtables_flatten(lst, font):
return self
def _Lookup_PairPosFormat2_subtables_flatten(lst, font):
- assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools."
+ assert allEqual([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools."
self = ot.PairPos()
self.Format = 2
@@ -603,8 +735,8 @@ def merge(merger, self, lst):
if not sts:
continue
if sts[0].__class__.__name__.startswith('Extension'):
- assert _all_equal([st.__class__ for st in sts])
- assert _all_equal([st.ExtensionLookupType for st in sts])
+ assert allEqual([st.__class__ for st in sts])
+ assert allEqual([st.ExtensionLookupType for st in sts])
l.LookupType = sts[0].ExtensionLookupType
new_sts = [st.ExtSubTable for st in sts]
del sts[:]
@@ -655,8 +787,17 @@ class InstancerMerger(AligningMerger):
self.location = location
self.scalars = model.getScalars(location)
+@InstancerMerger.merger(ot.CaretValue)
+def merge(merger, self, lst):
+ assert self.Format == 1
+ Coords = [a.Coordinate for a in lst]
+ model = merger.model
+ scalars = merger.scalars
+ self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars))
+
@InstancerMerger.merger(ot.Anchor)
def merge(merger, self, lst):
+ assert self.Format == 1
XCoords = [a.XCoordinate for a in lst]
YCoords = [a.YCoordinate for a in lst]
model = merger.model
@@ -688,7 +829,9 @@ def merge(merger, self, lst):
class MutatorMerger(AligningMerger):
"""A merger that takes a variable font, and instantiates
- an instance."""
+ an instance. While there's no "merging" to be done per se,
+ the operation can benefit from many operations that the
+ aligning merger does."""
def __init__(self, font, location):
Merger.__init__(self, font)
@@ -702,59 +845,32 @@ class MutatorMerger(AligningMerger):
self.instancer = VarStoreInstancer(store, font['fvar'].axes, location)
- def instantiate(self):
- font = self.font
-
- for tableTag in 'GSUB','GPOS':
- if not tableTag in font:
- continue
- table = font[tableTag].table
- if not hasattr(table, 'FeatureVariations'):
- continue
- variations = table.FeatureVariations
- for record in variations.FeatureVariationRecord:
- applies = True
- for condition in record.ConditionSet.ConditionTable:
- if condition.Format == 1:
- axisIdx = condition.AxisIndex
- axisTag = self.font['fvar'].axes[axisIdx].axisTag
- Min = condition.FilterRangeMinValue
- Max = condition.FilterRangeMaxValue
- loc = self.location[axisTag]
- if not (Min <= loc <= Max):
- applies = False
- else:
- applies = False
- if not applies:
- break
-
- if applies:
- assert record.FeatureTableSubstitution.Version == 0x00010000
- for rec in record.FeatureTableSubstitution.SubstitutionRecord:
- table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature
- break
- del table.FeatureVariations
-
-
- self.mergeTables(font, [font], ['GPOS'])
+@MutatorMerger.merger(ot.CaretValue)
+def merge(merger, self, lst):
+
+ # Hack till we become selfless.
+ self.__dict__ = lst[0].__dict__.copy()
- if 'GDEF' in font:
- gdef = font['GDEF'].table
- if gdef.Version >= 0x00010003:
- del gdef.VarStore
- gdef.Version = 0x00010002
- if gdef.MarkGlyphSetsDef is None:
- del gdef.MarkGlyphSetsDef
- gdef.Version = 0x00010000
- if not (gdef.LigCaretList or
- gdef.MarkAttachClassDef or
- gdef.GlyphClassDef or
- gdef.AttachList or
- (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
- del font['GDEF']
+ if self.Format != 3:
+ return
+
+ instancer = merger.instancer
+ dev = self.DeviceTable
+ del self.DeviceTable
+ if dev:
+ assert dev.DeltaFormat == 0x8000
+ varidx = (dev.StartSize << 16) + dev.EndSize
+ delta = otRound(instancer[varidx])
+ self.Coordinate += delta
+
+ self.Format = 1
@MutatorMerger.merger(ot.Anchor)
def merge(merger, self, lst):
+
+ # Hack till we become selfless.
+ self.__dict__ = lst[0].__dict__.copy()
+
if self.Format != 3:
return
@@ -780,9 +896,7 @@ def merge(merger, self, lst):
@MutatorMerger.merger(otBase.ValueRecord)
def merge(merger, self, lst):
- # All other structs are merged with self pointing to a copy of base font,
- # except for ValueRecords which are sometimes created later and initialized
- # to have 0/None members. Hence the copy.
+ # Hack till we become selfless.
self.__dict__ = lst[0].__dict__.copy()
instancer = merger.instancer
@@ -816,26 +930,43 @@ class VariationMerger(AligningMerger):
def __init__(self, model, axisTags, font):
Merger.__init__(self, font)
- self.model = model
self.store_builder = varStore.OnlineVarStoreBuilder(axisTags)
+ self.setModel(model)
+
+ def setModel(self, model):
+ self.model = model
self.store_builder.setModel(model)
-def _all_equal(lst):
- if not lst:
- return True
- it = iter(lst)
- v0 = next(it)
- for v in it:
- if v0 != v:
- return False
- return True
+ def mergeThings(self, out, lst):
+ masterModel = None
+ if None in lst:
+ if allNone(lst):
+ assert out is None, (out, lst)
+ return
+ masterModel = self.model
+ model, lst = masterModel.getSubModel(lst)
+ self.setModel(model)
+
+ super(VariationMerger, self).mergeThings(out, lst)
+
+ if masterModel:
+ self.setModel(masterModel)
+
def buildVarDevTable(store_builder, master_values):
- if _all_equal(master_values):
+ if allEqual(master_values):
return master_values[0], None
base, varIdx = store_builder.storeMasters(master_values)
return base, builder.buildVarDevTable(varIdx)
+@VariationMerger.merger(ot.CaretValue)
+def merge(merger, self, lst):
+ assert self.Format == 1
+ self.Coordinate, DeviceTable = buildVarDevTable(merger.store_builder, [a.Coordinate for a in lst])
+ if DeviceTable:
+ self.Format = 3
+ self.DeviceTable = DeviceTable
+
@VariationMerger.merger(ot.Anchor)
def merge(merger, self, lst):
assert self.Format == 1
diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py
index 653b75ff..207e30f3 100644
--- a/Lib/fontTools/varLib/models.py
+++ b/Lib/fontTools/varLib/models.py
@@ -2,9 +2,36 @@
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
-__all__ = ['normalizeValue', 'normalizeLocation', 'supportScalar', 'VariationModel']
+__all__ = ['nonNone', 'allNone', 'allEqual', 'allEqualTo', 'subList',
+ 'normalizeValue', 'normalizeLocation',
+ 'supportScalar',
+ 'VariationModel']
+def nonNone(lst):
+ return [l for l in lst if l is not None]
+
+def allNone(lst):
+ return all(l is None for l in lst)
+
+def allEqualTo(ref, lst, mapper=None):
+ if mapper is None:
+ return all(ref == item for item in lst)
+ else:
+ mapped = mapper(ref)
+ return all(mapped == mapper(item) for item in lst)
+
+def allEqual(lst, mapper=None):
+ if not lst:
+ return True
+ it = iter(lst)
+ first = next(it)
+ return allEqualTo(first, it, mapper=mapper)
+
+def subList(truth, lst):
+ assert len(truth) == len(lst)
+ return [l for l,t in zip(lst,truth) if t]
+
def normalizeValue(v, triple):
"""Normalizes value based on a min/default/max triple.
>>> normalizeValue(400, (100, 400, 900))
@@ -163,6 +190,9 @@ class VariationModel(object):
"""
def __init__(self, locations, axisOrder=[]):
+ self.origLocations = locations
+ self.axisOrder = axisOrder
+
locations = [{k:v for k,v in loc.items() if v != 0.} for loc in locations]
keyFunc = self.getMasterLocationsSortKeyFunc(locations, axisOrder=axisOrder)
axisPoints = keyFunc.axisPoints
@@ -172,6 +202,17 @@ class VariationModel(object):
self.reverseMapping = [locations.index(l) for l in self.locations] # Reverse of above
self._computeMasterSupports(axisPoints, axisOrder)
+ self._subModels = {}
+
+ def getSubModel(self, items):
+ if None not in items:
+ return self, items
+ key = tuple(v is not None for v in items)
+ subModel = self._subModels.get(key)
+ if subModel is None:
+ subModel = VariationModel(subList(key, self.origLocations), self.axisOrder)
+ self._subModels[key] = subModel
+ return subModel, subList(key, items)
@staticmethod
def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
@@ -223,24 +264,37 @@ class VariationModel(object):
return min(v for v in lst if v > value)
else:
return value
+ def reorderMasters(self, master_list, mapping):
+ # For changing the master data order without
+ # recomputing supports and deltaWeights.
+ new_list = [master_list[idx] for idx in mapping]
+ self.origLocations = [self.origLocations[idx] for idx in mapping]
+ locations = [{k:v for k,v in loc.items() if v != 0.}
+ for loc in self.origLocations]
+ self.mapping = [self.locations.index(l) for l in locations]
+ self.reverseMapping = [locations.index(l) for l in self.locations]
+ self._subModels = {}
+ return new_list
def _computeMasterSupports(self, axisPoints, axisOrder):
supports = []
deltaWeights = []
locations = self.locations
+ # Compute min/max across each axis, use it as total range.
+ # TODO Take this as input from outside?
+ minV = {}
+ maxV = {}
+ for l in locations:
+ for k,v in l.items():
+ minV[k] = min(v, minV.get(k, v))
+ maxV[k] = max(v, maxV.get(k, v))
for i,loc in enumerate(locations):
box = {}
-
- # Account for axisPoints first
- # TODO Use axis min/max instead? Isn't that always -1/+1?
- for axis,values in axisPoints.items():
- if not axis in loc:
- continue
- locV = loc[axis]
+ for axis,locV in loc.items():
if locV > 0:
- box[axis] = (0, locV, max({locV}|values))
+ box[axis] = (0, locV, maxV[axis])
else:
- box[axis] = (min({locV}|values), locV, 0)
+ box[axis] = (minV[axis], locV, 0)
locAxes = set(loc.keys())
# Walk over previous masters now
@@ -258,12 +312,15 @@ class VariationModel(object):
continue
# Split the box for new master; split in whatever direction
- # that has largest range ratio. See commit for details.
- orderedAxes = [axis for axis in axisOrder if axis in m.keys()]
- orderedAxes.extend([axis for axis in sorted(m.keys()) if axis not in axisOrder])
- bestAxis = None
+ # that has largest range ratio.
+ #
+ # For symmetry, we actually cut across multiple axes
+ # if they have the largest, equal, ratio.
+ # https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
+
+ bestAxes = {}
bestRatio = -1
- for axis in orderedAxes:
+ for axis in m.keys():
val = m[axis]
assert axis in box
lower,locV,upper = box[axis]
@@ -278,14 +335,13 @@ class VariationModel(object):
# Can't split box in this direction.
continue
if ratio > bestRatio:
+ bestAxes = {}
bestRatio = ratio
- bestAxis = axis
- bestLower = newLower
- bestUpper = newUpper
- bestLocV = locV
+ if ratio == bestRatio:
+ bestAxes[axis] = (newLower, locV, newUpper)
- if bestAxis:
- box[bestAxis] = (bestLower,bestLocV,bestUpper)
+ for axis,triple in bestAxes.items ():
+ box[axis] = triple
supports.append(box)
deltaWeight = {}
@@ -310,6 +366,10 @@ class VariationModel(object):
out.append(delta)
return out
+ def getDeltasAndSupports(self, items):
+ model, items = self.getSubModel(items)
+ return model.getDeltas(items), model.supports
+
def getScalars(self, loc):
return [supportScalar(loc, support) for support in self.supports]
diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py
index 7a03e454..1a3b7389 100644
--- a/Lib/fontTools/varLib/mutator.py
+++ b/Lib/fontTools/varLib/mutator.py
@@ -5,17 +5,22 @@ $ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85
"""
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
-from fontTools.misc.fixedTools import floatToFixedToFloat, otRound
-from fontTools.ttLib import TTFont
+from fontTools.misc.fixedTools import floatToFixedToFloat, otRound, floatToFixed
+from fontTools.pens.boundsPen import BoundsPen
+from fontTools.ttLib import TTFont, newTable
+from fontTools.ttLib.tables import ttProgram
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools.varLib import _GetCoordinates, _SetCoordinates
from fontTools.varLib.models import (
- supportScalar, normalizeLocation, piecewiseLinearMap
+ supportScalar,
+ normalizeLocation,
+ piecewiseLinearMap,
)
from fontTools.varLib.merger import MutatorMerger
from fontTools.varLib.varStore import VarStoreInstancer
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.iup import iup_delta
+import fontTools.subset.cff
import os.path
import logging
@@ -30,6 +35,116 @@ for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1):
OS2_WIDTH_CLASS_VALUES[half] = i
+def interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas):
+ pd_blend_lists = ("BlueValues", "OtherBlues", "FamilyBlues",
+ "FamilyOtherBlues", "StemSnapH",
+ "StemSnapV")
+ pd_blend_values = ("BlueScale", "BlueShift",
+ "BlueFuzz", "StdHW", "StdVW")
+ for fontDict in topDict.FDArray:
+ pd = fontDict.Private
+ vsindex = pd.vsindex if (hasattr(pd, 'vsindex')) else 0
+ for key, value in pd.rawDict.items():
+ if (key in pd_blend_values) and isinstance(value, list):
+ delta = interpolateFromDeltas(vsindex, value[1:])
+ pd.rawDict[key] = otRound(value[0] + delta)
+ elif (key in pd_blend_lists) and isinstance(value[0], list):
+ """If any argument in a BlueValues list is a blend list,
+ then they all are. The first value of each list is an
+ absolute value. The delta tuples are calculated from
+ relative master values, hence we need to append all the
+ deltas to date to each successive absolute value."""
+ delta = 0
+ for i, val_list in enumerate(value):
+ delta += otRound(interpolateFromDeltas(vsindex,
+ val_list[1:]))
+ value[i] = val_list[0] + delta
+
+
+def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
+ charstrings = topDict.CharStrings
+ for gname in glyphOrder:
+ # Interpolate charstring
+ charstring = charstrings[gname]
+ pd = charstring.private
+ vsindex = pd.vsindex if (hasattr(pd, 'vsindex')) else 0
+ num_regions = pd.getNumRegions(vsindex)
+ numMasters = num_regions + 1
+ new_program = []
+ last_i = 0
+ for i, token in enumerate(charstring.program):
+ if token == 'blend':
+ num_args = charstring.program[i - 1]
+ """ The stack is now:
+ ..args for following operations
+ num_args values from the default font
+ num_args tuples, each with numMasters-1 delta values
+ num_blend_args
+ 'blend'
+ """
+ argi = i - (num_args*numMasters + 1)
+ end_args = tuplei = argi + num_args
+ while argi < end_args:
+ next_ti = tuplei + num_regions
+ deltas = charstring.program[tuplei:next_ti]
+ delta = interpolateFromDeltas(vsindex, deltas)
+ charstring.program[argi] += otRound(delta)
+ tuplei = next_ti
+ argi += 1
+ new_program.extend(charstring.program[last_i:end_args])
+ last_i = i + 1
+ if last_i != 0:
+ new_program.extend(charstring.program[last_i:])
+ charstring.program = new_program
+
+
+def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
+ """Unlike TrueType glyphs, neither advance width nor bounding box
+ info is stored in a CFF2 charstring. The width data exists only in
+ the hmtx and HVAR tables. Since LSB data cannot be interpolated
+ reliably from the master LSB values in the hmtx table, we traverse
+ the charstring to determine the actual bound box. """
+
+ charstrings = topDict.CharStrings
+ boundsPen = BoundsPen(glyphOrder)
+ hmtx = varfont['hmtx']
+ hvar_table = None
+ if 'HVAR' in varfont:
+ hvar_table = varfont['HVAR'].table
+ fvar = varfont['fvar']
+ varStoreInstancer = VarStoreInstancer(hvar_table.VarStore, fvar.axes, loc)
+
+ for gid, gname in enumerate(glyphOrder):
+ entry = list(hmtx[gname])
+ # get width delta.
+ if hvar_table:
+ if hvar_table.AdvWidthMap:
+ width_idx = hvar_table.AdvWidthMap.mapping[gname]
+ else:
+ width_idx = gid
+ width_delta = otRound(varStoreInstancer[width_idx])
+ else:
+ width_delta = 0
+
+ # get LSB.
+ boundsPen.init()
+ charstring = charstrings[gname]
+ charstring.draw(boundsPen)
+ if boundsPen.bounds is None:
+ # Happens with non-marking glyphs
+ lsb_delta = 0
+ else:
+ lsb = boundsPen.bounds[0]
+ lsb_delta = entry[1] - lsb
+
+ if lsb_delta or width_delta:
+ if width_delta:
+ entry[0] += width_delta
+ if lsb_delta:
+ entry[1] = lsb
+ hmtx[gname] = tuple(entry)
+
+
def instantiateVariableFont(varfont, location, inplace=False):
""" Generate a static instance from a variable TTFont and a dictionary
defining the desired location along the variable font's axes.
@@ -58,31 +173,34 @@ def instantiateVariableFont(varfont, location, inplace=False):
# Location is normalized now
log.info("Normalized location: %s", loc)
- log.info("Mutating glyf/gvar tables")
- gvar = varfont['gvar']
- glyf = varfont['glyf']
- # get list of glyph names in gvar sorted by component depth
- glyphnames = sorted(
- gvar.variations.keys(),
- key=lambda name: (
- glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
- if glyf[name].isComposite() else 0,
- name))
- for glyphname in glyphnames:
- variations = gvar.variations[glyphname]
- coordinates,_ = _GetCoordinates(varfont, glyphname)
- origCoords, endPts = None, None
- for var in variations:
- scalar = supportScalar(loc, var.axes)
- if not scalar: continue
- delta = var.coordinates
- if None in delta:
- if origCoords is None:
- origCoords,control = _GetCoordinates(varfont, glyphname)
- endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
- delta = iup_delta(delta, origCoords, endPts)
- coordinates += GlyphCoordinates(delta) * scalar
- _SetCoordinates(varfont, glyphname, coordinates)
+ if 'gvar' in varfont:
+ log.info("Mutating glyf/gvar tables")
+ gvar = varfont['gvar']
+ glyf = varfont['glyf']
+ # get list of glyph names in gvar sorted by component depth
+ glyphnames = sorted(
+ gvar.variations.keys(),
+ key=lambda name: (
+ glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
+ if glyf[name].isComposite() else 0,
+ name))
+ for glyphname in glyphnames:
+ variations = gvar.variations[glyphname]
+ coordinates,_ = _GetCoordinates(varfont, glyphname)
+ origCoords, endPts = None, None
+ for var in variations:
+ scalar = supportScalar(loc, var.axes)
+ if not scalar: continue
+ delta = var.coordinates
+ if None in delta:
+ if origCoords is None:
+ origCoords,control = _GetCoordinates(varfont, glyphname)
+ endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
+ delta = iup_delta(delta, origCoords, endPts)
+ coordinates += GlyphCoordinates(delta) * scalar
+ _SetCoordinates(varfont, glyphname, coordinates)
+ else:
+ glyf = None
if 'cvar' in varfont:
log.info("Mutating cvt/cvar tables")
@@ -98,6 +216,20 @@ def instantiateVariableFont(varfont, location, inplace=False):
for i, delta in deltas.items():
cvt[i] += otRound(delta)
+ if 'CFF2' in varfont:
+ log.info("Mutating CFF2 table")
+ glyphOrder = varfont.getGlyphOrder()
+ CFF2 = varfont['CFF2']
+ topDict = CFF2.cff.topDictIndex[0]
+ vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc)
+ interpolateFromDeltas = vsInstancer.interpolateFromDeltas
+ interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
+ CFF2.desubroutinize()
+ interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder)
+ interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
+ del topDict.rawDict['VarStore']
+ del topDict.VarStore
+
if 'MVAR' in varfont:
log.info("Mutating MVAR table")
mvar = varfont['MVAR'].table
@@ -114,12 +246,98 @@ def instantiateVariableFont(varfont, location, inplace=False):
setattr(varfont[tableTag], itemName,
getattr(varfont[tableTag], itemName) + delta)
- if 'GDEF' in varfont:
+ log.info("Mutating FeatureVariations")
+ for tableTag in 'GSUB','GPOS':
+ if not tableTag in varfont:
+ continue
+ table = varfont[tableTag].table
+ if not hasattr(table, 'FeatureVariations'):
+ continue
+ variations = table.FeatureVariations
+ for record in variations.FeatureVariationRecord:
+ applies = True
+ for condition in record.ConditionSet.ConditionTable:
+ if condition.Format == 1:
+ axisIdx = condition.AxisIndex
+ axisTag = fvar.axes[axisIdx].axisTag
+ Min = condition.FilterRangeMinValue
+ Max = condition.FilterRangeMaxValue
+ v = loc[axisTag]
+ if not (Min <= v <= Max):
+ applies = False
+ else:
+ applies = False
+ if not applies:
+ break
+
+ if applies:
+ assert record.FeatureTableSubstitution.Version == 0x00010000
+ for rec in record.FeatureTableSubstitution.SubstitutionRecord:
+ table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature
+ break
+ del table.FeatureVariations
+
+ if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003:
log.info("Mutating GDEF/GPOS/GSUB tables")
+ gdef = varfont['GDEF'].table
+ instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)
+
merger = MutatorMerger(varfont, loc)
+ merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS'])
+
+ # Downgrade GDEF.
+ del gdef.VarStore
+ gdef.Version = 0x00010002
+ if gdef.MarkGlyphSetsDef is None:
+ del gdef.MarkGlyphSetsDef
+ gdef.Version = 0x00010000
+
+ if not (gdef.LigCaretList or
+ gdef.MarkAttachClassDef or
+ gdef.GlyphClassDef or
+ gdef.AttachList or
+ (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
+ del varfont['GDEF']
+
+ addidef = False
+ if glyf:
+ for glyph in glyf.glyphs.values():
+ if hasattr(glyph, "program"):
+ instructions = glyph.program.getAssembly()
+ # If GETVARIATION opcode is used in bytecode of any glyph add IDEF
+ addidef = any(op.startswith("GETVARIATION") for op in instructions)
+ if addidef:
+ break
+ if addidef:
+ log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
+ asm = []
+ if 'fpgm' in varfont:
+ fpgm = varfont['fpgm']
+ asm = fpgm.program.getAssembly()
+ else:
+ fpgm = newTable('fpgm')
+ fpgm.program = ttProgram.Program()
+ varfont['fpgm'] = fpgm
+ asm.append("PUSHB[000] 145")
+ asm.append("IDEF[ ]")
+ args = [str(len(loc))]
+ for a in fvar.axes:
+ args.append(str(floatToFixed(loc[a.axisTag], 14)))
+ asm.append("NPUSHW[ ] " + ' '.join(args))
+ asm.append("ENDF[ ]")
+ fpgm.program.fromAssembly(asm)
- log.info("Building interpolated tables")
- merger.instantiate()
+ # Change maxp attributes as IDEF is added
+ if 'maxp' in varfont:
+ maxp = varfont['maxp']
+ if hasattr(maxp, "maxInstructionDefs"):
+ maxp.maxInstructionDefs += 1
+ else:
+ setattr(maxp, "maxInstructionDefs", 1)
+ if hasattr(maxp, "maxStackElements"):
+ maxp.maxStackElements = max(len(loc), maxp.maxStackElements)
+ else:
+ setattr(maxp, "maxInstructionDefs", len(loc))
if 'name' in varfont:
log.info("Pruning name table")
diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py
index a1ca7df6..66d0c95a 100644
--- a/Lib/fontTools/varLib/varStore.py
+++ b/Lib/fontTools/varLib/varStore.py
@@ -4,8 +4,7 @@ from fontTools.misc.fixedTools import otRound
from fontTools.ttLib.tables import otTables as ot
from fontTools.varLib.models import supportScalar
from fontTools.varLib.builder import (buildVarRegionList, buildVarStore,
- buildVarRegion, buildVarData,
- VarData_CalculateNumShorts)
+ buildVarRegion, buildVarData)
from functools import partial
from collections import defaultdict
from array import array
@@ -24,25 +23,36 @@ class OnlineVarStoreBuilder(object):
self._store = buildVarStore(self._regionList, [])
self._data = None
self._model = None
+ self._supports = None
+ self._varDataIndices = {}
+ self._varDataCaches = {}
self._cache = {}
def setModel(self, model):
+ self.setSupports(model.supports)
self._model = model
- self._cache = {} # Empty cached items
+
+ def setSupports(self, supports):
+ self._model = None
+ self._supports = list(supports)
+ if not self._supports[0]:
+ del self._supports[0] # Drop base master support
+ self._cache = {}
+ self._data = None
def finish(self, optimize=True):
self._regionList.RegionCount = len(self._regionList.Region)
self._store.VarDataCount = len(self._store.VarData)
for data in self._store.VarData:
data.ItemCount = len(data.Item)
- VarData_CalculateNumShorts(data, optimize)
+ data.calculateNumShorts(optimize=optimize)
return self._store
def _add_VarData(self):
regionMap = self._regionMap
regionList = self._regionList
- regions = self._model.supports[1:]
+ regions = self._supports
regionIndices = []
for region in regions:
key = _getLocationKey(region)
@@ -53,17 +63,46 @@ class OnlineVarStoreBuilder(object):
regionList.Region.append(varRegion)
regionIndices.append(idx)
- data = self._data = buildVarData(regionIndices, [], optimize=False)
- self._outer = len(self._store.VarData)
- self._store.VarData.append(data)
+ # Check if we have one already...
+ key = tuple(regionIndices)
+ varDataIdx = self._varDataIndices.get(key)
+ if varDataIdx is not None:
+ self._outer = varDataIdx
+ self._data = self._store.VarData[varDataIdx]
+ self._cache = self._varDataCaches[key]
+ if len(self._data.Item) == 0xFFF:
+ # This is full. Need new one.
+ varDataIdx = None
+
+ if varDataIdx is None:
+ self._data = buildVarData(regionIndices, [], optimize=False)
+ self._outer = len(self._store.VarData)
+ self._store.VarData.append(self._data)
+ self._varDataIndices[key] = self._outer
+ if key not in self._varDataCaches:
+ self._varDataCaches[key] = {}
+ self._cache = self._varDataCaches[key]
+
def storeMasters(self, master_values):
- deltas = [otRound(d) for d in self._model.getDeltas(master_values)]
- base = deltas.pop(0)
- deltas = tuple(deltas)
+ deltas = self._model.getDeltas(master_values)
+ base = otRound(deltas.pop(0))
+ return base, self.storeDeltas(deltas)
+
+ def storeDeltas(self, deltas):
+ # Pity that this exists here, since VarData_addItem
+ # does the same. But to look into our cache, it's
+ # good to adjust deltas here as well...
+ deltas = [otRound(d) for d in deltas]
+ if len(deltas) == len(self._supports) + 1:
+ deltas = tuple(deltas[1:])
+ else:
+ assert len(deltas) == len(self._supports)
+ deltas = tuple(deltas)
+
varIdx = self._cache.get(deltas)
if varIdx is not None:
- return base, varIdx
+ return varIdx
if not self._data:
self._add_VarData()
@@ -71,18 +110,34 @@ class OnlineVarStoreBuilder(object):
if inner == 0xFFFF:
# Full array. Start new one.
self._add_VarData()
- return self.storeMasters(master_values)
- self._data.Item.append(deltas)
+ return self.storeDeltas(deltas)
+ self._data.addItem(deltas)
varIdx = (self._outer << 16) + inner
self._cache[deltas] = varIdx
- return base, varIdx
+ return varIdx
+
+def VarData_addItem(self, deltas):
+ deltas = [otRound(d) for d in deltas]
+
+ countUs = self.VarRegionCount
+ countThem = len(deltas)
+ if countUs + 1 == countThem:
+ deltas = tuple(deltas[1:])
+ else:
+ assert countUs == countThem, (countUs, countThem)
+ deltas = tuple(deltas)
+ self.Item.append(list(deltas))
+ self.ItemCount = len(self.Item)
+ot.VarData.addItem = VarData_addItem
def VarRegion_get_support(self, fvar_axes):
return {fvar_axes[i].axisTag: (reg.StartCoord,reg.PeakCoord,reg.EndCoord)
for i,reg in enumerate(self.VarRegionAxis)}
+ot.VarRegion.get_support = VarRegion_get_support
+
class VarStoreInstancer(object):
def __init__(self, varstore, fvar_axes, location={}):
@@ -102,23 +157,31 @@ class VarStoreInstancer(object):
def _getScalar(self, regionIdx):
scalar = self._scalars.get(regionIdx)
if scalar is None:
- support = VarRegion_get_support(self._regions[regionIdx], self.fvar_axes)
+ support = self._regions[regionIdx].get_support(self.fvar_axes)
scalar = supportScalar(self.location, support)
self._scalars[regionIdx] = scalar
return scalar
- def __getitem__(self, varidx):
+ @staticmethod
+ def interpolateFromDeltasAndScalars(deltas, scalars):
+ delta = 0.
+ for d,s in zip(deltas, scalars):
+ if not s: continue
+ delta += d * s
+ return delta
+ def __getitem__(self, varidx):
major, minor = varidx >> 16, varidx & 0xFFFF
-
varData = self._varData
scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex]
-
deltas = varData[major].Item[minor]
- delta = 0.
- for d,s in zip(deltas, scalars):
- delta += d * s
- return delta
+ return self.interpolateFromDeltasAndScalars(deltas, scalars)
+
+ def interpolateFromDeltas(self, varDataIndex, deltas):
+ varData = self._varData
+ scalars = [self._getScalar(ri) for ri in
+ varData[varDataIndex].VarRegionIndex]
+ return self.interpolateFromDeltasAndScalars(deltas, scalars)
#
@@ -149,7 +212,7 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True):
usedMinors = used.get(major)
if usedMinors is None:
continue
- newMajor = varDataMap[major] = len(newVarData)
+ newMajor = len(newVarData)
newVarData.append(data)
items = data.Item
@@ -162,8 +225,7 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True):
data.Item = newItems
data.ItemCount = len(data.Item)
- if optimize:
- VarData_CalculateNumShorts(data)
+ data.calculateNumShorts(optimize=optimize)
self.VarData = newVarData
self.VarDataCount = len(self.VarData)
@@ -201,27 +263,26 @@ def VarStore_prune_regions(self):
ot.VarStore.prune_regions = VarStore_prune_regions
-def _visit(self, objType, func):
- """Recurse down from self, if type of an object is objType,
- call func() on it. Only works for otData-style classes."""
+def _visit(self, func):
+ """Recurse down from self, if type of an object is ot.Device,
+ call func() on it. Works on otData-style classes."""
- if type(self) == objType:
+ if type(self) == ot.Device:
func(self)
- return # We don't recurse down; don't need to.
- if isinstance(self, list):
+ elif isinstance(self, list):
for that in self:
- _visit(that, objType, func)
+ _visit(that, func)
- if hasattr(self, 'getConverters'):
+ elif hasattr(self, 'getConverters') and not hasattr(self, 'postRead'):
for conv in self.getConverters():
that = getattr(self, conv.name, None)
if that is not None:
- _visit(that, objType, func)
+ _visit(that, func)
- if isinstance(self, ot.ValueRecord):
+ elif isinstance(self, ot.ValueRecord):
for that in self.__dict__.values():
- _visit(that, objType, func)
+ _visit(that, func)
def _Device_recordVarIdx(self, s):
"""Add VarIdx in this Device table (if any) to the set s."""
@@ -230,13 +291,13 @@ def _Device_recordVarIdx(self, s):
def Object_collect_device_varidxes(self, varidxes):
adder = partial(_Device_recordVarIdx, s=varidxes)
- _visit(self, ot.Device, adder)
+ _visit(self, adder)
ot.GDEF.collect_device_varidxes = Object_collect_device_varidxes
ot.GPOS.collect_device_varidxes = Object_collect_device_varidxes
def _Device_mapVarIdx(self, mapping, done):
- """Add VarIdx in this Device table (if any) to the set s."""
+ """Map VarIdx in this Device table (if any) through mapping."""
if id(self) in done:
return
done.add(id(self))
@@ -247,7 +308,7 @@ def _Device_mapVarIdx(self, mapping, done):
def Object_remap_device_varidxes(self, varidxes_map):
mapper = partial(_Device_mapVarIdx, mapping=varidxes_map, done=set())
- _visit(self, ot.Device, mapper)
+ _visit(self, mapper)
ot.GDEF.remap_device_varidxes = Object_remap_device_varidxes
ot.GPOS.remap_device_varidxes = Object_remap_device_varidxes
@@ -464,7 +525,7 @@ def VarStore_optimize(self):
self.VarDataCount = len(self.VarData)
for data in self.VarData:
data.ItemCount = len(data.Item)
- VarData_CalculateNumShorts(data)
+ data.optimize()
return varidx_map
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index ee3a3772..32f95682 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.31.0
+Version: 3.35.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -28,6 +28,13 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4
or later.
+ **NOTE** After January 1 2019, until no later than June 30 2019, the support
+ for *Python 2.7* will be limited to only bug fixes, and no new features will
+ be added to the ``py27`` branch. The upcoming FontTools 4.x series will require
+ *Python 3.5* or above. You can read more `here <https://python3statement.org>`__
+ and `here <https://github.com/fonttools/fonttools/issues/765>`__ for the
+ reasons behind this decision.
+
The package is listed in the Python Package Index (PyPI), so you can
install it with `pip <https://pip.pypa.io>`__:
@@ -255,6 +262,14 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
*Extra:* ``interpolatable``
+ - ``Lib/fontTools/varLib/plot.py``
+
+ Module for visualizing DesignSpaceDocument and resulting VariationModel.
+
+ * `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library.
+
+ *Extra:* ``plot``
+
- ``Lib/fontTools/misc/symfont.py``
Advanced module for symbolic font statistics analysis; it requires:
@@ -314,7 +329,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
installed ``fontTools`` package, or the first one found in the
``PYTHONPATH``.
- You can also use `tox <https://testrun.org/tox/latest/>`__ to
+ You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
automatically run tests on different Python versions in isolated virtual
environments.
@@ -415,6 +430,82 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
Changelog
~~~~~~~~~
+ 3.35.0 (released 2019-01-07)
+ ----------------------------
+
+ - [psCharStrings] In ``encodeFloat`` function, use float's "general format" with
+ 8 digits of precision (i.e. ``%8g``) instead of ``str()``. This works around
+ a macOS rendering issue when real numbers in CFF table are too long, and
+ also makes sure that floats are encoded with the same precision in python 2.7
+ and 3.x (#1430, googlei18n/ufo2ft#306).
+ - [_n_a_m_e/fontBuilder] Make ``_n_a_m_e_table.addMultilingualName`` also add
+ Macintosh (platformID=1) names by default. Added options to ``FontBuilder``
+ ``setupNameTable`` method to optionally disable Macintosh or Windows names.
+ (#1359, #1431).
+ - [varLib] Make ``build`` optionally accept a ``DesignSpaceDocument`` object,
+ instead of a designspace file path. The caller can now set the ``font``
+ attribute of designspace's sources to a TTFont object, thus allowing to
+ skip filenames manipulation altogether (#1416, #1425).
+ - [sfnt] Allow SFNTReader objects to be deep-copied.
+ - Require typing>=3.6.4 on py27 to fix issue with singledispatch (#1423).
+ - [designspaceLib/t1Lib/macRes] Fixed some cases where pathlib.Path objects were
+ not accepted (#1421).
+ - [varLib] Fixed merging of multiple PairPosFormat2 subtables (#1411).
+ - [varLib] The default STAT table version is now set to 1.1, to improve
+ compatibility with legacy applications (#1413).
+
+ 3.34.2 (released 2018-12-17)
+ ----------------------------
+
+ - [merge] Fixed AssertionError when none of the script tables in GPOS/GSUB have
+ a DefaultLangSys record (#1408, 135a4a1).
+
+ 3.34.1 (released 2018-12-17)
+ ----------------------------
+
+ - [varLib] Work around macOS rendering issue for composites without gvar entry (#1381).
+
+ 3.34.0 (released 2018-12-14)
+ ----------------------------
+
+ - [varLib] Support generation of CFF2 variable fonts. ``model.reorderMasters()``
+ now supports arbitrary mapping. Fix handling of overlapping ranges for feature
+ variations (#1400).
+ - [cffLib, subset] Code clean-up and fixing related to CFF2 support.
+ - [ttLib.tables.ttProgram] Use raw strings for regex patterns (#1389).
+ - [fontbuilder] Initial support for building CFF2 fonts. Set CFF's
+ ``FontMatrix`` automatically from unitsPerEm.
+ - [plistLib] Accept the more general ``collections.Mapping`` instead of the
+ specific ``dict`` class to support custom data classes that should serialize
+ to dictionaries.
+
+ 3.33.0 (released 2018-11-30)
+ ----------------------------
+ - [subset] subsetter bug fix with variable fonts.
+ - [varLib.featureVar] Improve FeatureVariations generation with many rules.
+ - [varLib] Enable sparse masters when building variable fonts:
+ https://github.com/fonttools/fonttools/pull/1368#issuecomment-437257368
+ - [varLib.mutator] Add IDEF for GETVARIATION opcode, for handling hints in an
+ instance.
+ - [ttLib] Ignore the length of kern table subtable format 0
+
+ 3.32.0 (released 2018-11-01)
+ ----------------------------
+
+ - [ufoLib] Make ``UFOWriter`` a subclass of ``UFOReader``, and use mixins
+ for shared methods (#1344).
+ - [featureVars] Fixed normalization error when a condition's minimum/maximum
+ attributes are missing in designspace ``<rule>`` (#1366).
+ - [setup.py] Added ``[plot]`` to extras, to optionally install ``matplotlib``,
+ needed to use the ``fonTools.varLib.plot`` module.
+ - [varLib] Take total bounding box into account when resolving model (7ee81c8).
+ If multiple axes have the same range ratio, cut across both (62003f4).
+ - [subset] Don't error if ``STAT`` has no ``AxisValue`` tables.
+ - [fontBuilder] Added a new submodule which contains a ``FontBuilder`` wrapper
+ class around ``TTFont`` that makes it easier to create a working TTF or OTF
+ font from scratch with code. NOTE: the API is still experimental and may
+ change in future versions.
+
3.31.0 (released 2018-10-21)
----------------------------
@@ -1491,11 +1582,13 @@ Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
-Provides-Extra: type1
+Provides-Extra: all
+Provides-Extra: interpolatable
+Provides-Extra: woff
Provides-Extra: lxml
Provides-Extra: unicode
-Provides-Extra: symfont
-Provides-Extra: all
+Provides-Extra: graphite
Provides-Extra: ufo
-Provides-Extra: woff
-Provides-Extra: interpolatable
+Provides-Extra: type1
+Provides-Extra: plot
+Provides-Extra: symfont
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index ef8491df..04db0956 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -87,6 +87,7 @@ Lib/fontTools/__init__.py
Lib/fontTools/__main__.py
Lib/fontTools/afmLib.py
Lib/fontTools/agl.py
+Lib/fontTools/fontBuilder.py
Lib/fontTools/inspect.py
Lib/fontTools/merge.py
Lib/fontTools/ttx.py
@@ -111,11 +112,13 @@ Lib/fontTools/misc/arrayTools.py
Lib/fontTools/misc/bezierTools.py
Lib/fontTools/misc/classifyTools.py
Lib/fontTools/misc/cliTools.py
+Lib/fontTools/misc/dictTools.py
Lib/fontTools/misc/eexec.py
Lib/fontTools/misc/encodingTools.py
Lib/fontTools/misc/etree.py
Lib/fontTools/misc/filenames.py
Lib/fontTools/misc/fixedTools.py
+Lib/fontTools/misc/intTools.py
Lib/fontTools/misc/loggingTools.py
Lib/fontTools/misc/macCreatorType.py
Lib/fontTools/misc/macRes.py
@@ -159,6 +162,7 @@ Lib/fontTools/pens/ttGlyphPen.py
Lib/fontTools/pens/wxPen.py
Lib/fontTools/subset/__init__.py
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/parser.py
@@ -285,6 +289,7 @@ Lib/fontTools/unicodedata/__init__.py
Lib/fontTools/varLib/__init__.py
Lib/fontTools/varLib/__main__.py
Lib/fontTools/varLib/builder.py
+Lib/fontTools/varLib/cff.py
Lib/fontTools/varLib/featureVars.py
Lib/fontTools/varLib/interpolatable.py
Lib/fontTools/varLib/interpolate_layout.py
@@ -520,6 +525,11 @@ Tests/feaLib/data/include/include6.fea
Tests/feaLib/data/include/includemissingfile.fea
Tests/feaLib/data/include/includeself.fea
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_var.otf.ttx
+Tests/fontBuilder/data/test_var.ttf.ttx
Tests/misc/arrayTools_test.py
Tests/misc/bezierTools_test.py
Tests/misc/classifyTools_test.py
@@ -609,6 +619,7 @@ Tests/pens/basePen_test.py
Tests/pens/boundsPen_test.py
Tests/pens/perimeterPen_test.py
Tests/pens/pointInsidePen_test.py
+Tests/pens/pointPen_test.py
Tests/pens/recordingPen_test.py
Tests/pens/reverseContourPen_test.py
Tests/pens/t2CharStringPen_test.py
@@ -1383,6 +1394,7 @@ Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/fo
Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt
Tests/varLib/__init__.py
Tests/varLib/builder_test.py
+Tests/varLib/featureVars_test.py
Tests/varLib/interpolatable_test.py
Tests/varLib/interpolate_layout_test.py
Tests/varLib/models_test.py
@@ -1392,10 +1404,17 @@ Tests/varLib/data/Build.designspace
Tests/varLib/data/BuildAvarEmptyAxis.designspace
Tests/varLib/data/BuildAvarIdentityMaps.designspace
Tests/varLib/data/BuildAvarSingleAxis.designspace
+Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace
Tests/varLib/data/FeatureVars.designspace
Tests/varLib/data/InterpolateLayout.designspace
Tests/varLib/data/InterpolateLayout2.designspace
Tests/varLib/data/InterpolateLayout3.designspace
+Tests/varLib/data/TestCFF2.designspace
+Tests/varLib/data/TestCFF2VF.otf
+Tests/varLib/data/master_cff2/TestCFF2_Black.otf
+Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otf
+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/TestFamily-Master0.ttx
@@ -1413,6 +1432,8 @@ Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedSemiBold.tt
Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Light.ttx
Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Regular.ttx
Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-SemiBold.ttx
+Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx
+Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx
Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx
Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/features.fea
Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/fontinfo.plist
@@ -1635,11 +1656,53 @@ Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/n.glif
Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/o.glif
Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/s.glif
Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/t.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/features.fea
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif
+Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif
+Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif
Tests/varLib/data/test_results/Build.ttx
Tests/varLib/data/test_results/BuildAvarEmptyAxis.ttx
Tests/varLib/data/test_results/BuildAvarIdentityMaps.ttx
Tests/varLib/data/test_results/BuildAvarSingleAxis.ttx
+Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx
Tests/varLib/data/test_results/BuildMain.ttx
+Tests/varLib/data/test_results/BuildTestCFF2.ttx
Tests/varLib/data/test_results/FeatureVars.ttx
Tests/varLib/data/test_results/InterpolateLayout.ttx
Tests/varLib/data/test_results/InterpolateLayout2.ttx
@@ -1664,7 +1727,9 @@ Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx
Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx
Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx
Tests/varLib/data/test_results/InterpolateLayoutMain.ttx
+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/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 c7eb2e13..9f927643 100644
--- a/Lib/fonttools.egg-info/requires.txt
+++ b/Lib/fonttools.egg-info/requires.txt
@@ -3,6 +3,8 @@
fs<3,>=2.1.1
lxml<5,>=4.0
zopfli>=0.1.4
+lz4>=1.7.4.2
+matplotlib
sympy
[all:platform_python_implementation != "PyPy"]
@@ -16,6 +18,7 @@ munkres
[all:python_version < "3.4"]
enum34>=1.1.6
singledispatch>=3.4.0.3
+typing>=3.6.4
[all:python_version < "3.7" and platform_python_implementation != "PyPy"]
unicodedata2>=11.0.0
@@ -23,6 +26,9 @@ unicodedata2>=11.0.0
[all:sys_platform == "darwin"]
xattr
+[graphite]
+lz4>=1.7.4.2
+
[interpolatable]
[interpolatable:platform_python_implementation != "PyPy"]
@@ -36,6 +42,10 @@ lxml<5,>=4.0
[lxml:python_version < "3.4"]
singledispatch>=3.4.0.3
+typing>=3.6.4
+
+[plot]
+matplotlib
[symfont]
sympy
diff --git a/METADATA b/METADATA
index f017a666..ba000aa7 100644
--- a/METADATA
+++ b/METADATA
@@ -7,12 +7,12 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/releases/download/3.31.0/fonttools-3.31.0.zip"
+ value: "https://github.com/fonttools/fonttools/releases/download/3.35.0/fonttools-3.35.0.zip"
}
- version: "3.31.0"
+ version: "3.35.0"
last_upgrade_date {
- year: 2018
- month: 10
- day: 30
+ year: 2019
+ month: 1
+ day: 8
}
}
diff --git a/MetaTools/buildTableList.py b/MetaTools/buildTableList.py
index de8a039d..eb9fb858 100755..100644
--- a/MetaTools/buildTableList.py
+++ b/MetaTools/buildTableList.py
@@ -30,9 +30,9 @@ modules.sort()
tables.sort()
-file = open(os.path.join(tablesDir, "__init__.py"), "w")
+with open(os.path.join(tablesDir, "__init__.py"), "w") as file:
-file.write('''
+ file.write('''
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
@@ -45,21 +45,20 @@ def _moduleFinderHint():
"""
''')
-for module in modules:
- file.write("\tfrom . import %s\n" % module)
+ for module in modules:
+ file.write("\tfrom . import %s\n" % module)
-file.write('''
+ file.write('''
if __name__ == "__main__":
import doctest, sys
sys.exit(doctest.testmod().failed)
''')
-file.close()
-
begin = ".. begin table list\n.. code::\n"
end = ".. end table list"
-doc = open(docFile).read()
+with open(docFile) as f:
+ doc = f.read()
beginPos = doc.find(begin)
assert beginPos > 0
beginPos = beginPos + len(begin) + 1
@@ -70,4 +69,5 @@ blockquote = "\n".join(" "*4 + line for line in lines) + "\n"
doc = doc[:beginPos] + blockquote + doc[endPos:]
-open(docFile, "w").write(doc)
+with open(docFile, "w") as f:
+ f.write(doc)
diff --git a/MetaTools/buildUCD.py b/MetaTools/buildUCD.py
index 12bd58f1..12bd58f1 100755..100644
--- a/MetaTools/buildUCD.py
+++ b/MetaTools/buildUCD.py
diff --git a/MetaTools/roundTrip.py b/MetaTools/roundTrip.py
index 122b39b4..648bc9d9 100755..100644
--- a/MetaTools/roundTrip.py
+++ b/MetaTools/roundTrip.py
@@ -74,23 +74,22 @@ def main(args):
if not files:
usage()
- report = open("report.txt", "a+")
- options = ttx.Options(rawOptions, len(files))
- for ttFile in files:
- try:
- roundTrip(ttFile, options, report)
- except KeyboardInterrupt:
- print("(Cancelled)")
- break
- except:
- print("*** round tripping aborted ***")
- traceback.print_exc()
- report.write("=============================================================\n")
- report.write(" An exception occurred while round tripping")
- report.write(" \"%s\"\n" % ttFile)
- traceback.print_exc(file=report)
- report.write("-------------------------------------------------------------\n")
- report.close()
+ with open("report.txt", "a+") as report:
+ options = ttx.Options(rawOptions, len(files))
+ for ttFile in files:
+ try:
+ roundTrip(ttFile, options, report)
+ except KeyboardInterrupt:
+ print("(Cancelled)")
+ break
+ except:
+ print("*** round tripping aborted ***")
+ traceback.print_exc()
+ report.write("=============================================================\n")
+ report.write(" An exception occurred while round tripping")
+ report.write(" \"%s\"\n" % ttFile)
+ traceback.print_exc(file=report)
+ report.write("-------------------------------------------------------------\n")
main(sys.argv[1:])
diff --git a/NEWS.rst b/NEWS.rst
index 16c2a906..59bd94f4 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,79 @@
+3.35.0 (released 2019-01-07)
+----------------------------
+
+- [psCharStrings] In ``encodeFloat`` function, use float's "general format" with
+ 8 digits of precision (i.e. ``%8g``) instead of ``str()``. This works around
+ a macOS rendering issue when real numbers in CFF table are too long, and
+ also makes sure that floats are encoded with the same precision in python 2.7
+ and 3.x (#1430, googlei18n/ufo2ft#306).
+- [_n_a_m_e/fontBuilder] Make ``_n_a_m_e_table.addMultilingualName`` also add
+ Macintosh (platformID=1) names by default. Added options to ``FontBuilder``
+ ``setupNameTable`` method to optionally disable Macintosh or Windows names.
+ (#1359, #1431).
+- [varLib] Make ``build`` optionally accept a ``DesignSpaceDocument`` object,
+ instead of a designspace file path. The caller can now set the ``font``
+ attribute of designspace's sources to a TTFont object, thus allowing to
+ skip filenames manipulation altogether (#1416, #1425).
+- [sfnt] Allow SFNTReader objects to be deep-copied.
+- Require typing>=3.6.4 on py27 to fix issue with singledispatch (#1423).
+- [designspaceLib/t1Lib/macRes] Fixed some cases where pathlib.Path objects were
+ not accepted (#1421).
+- [varLib] Fixed merging of multiple PairPosFormat2 subtables (#1411).
+- [varLib] The default STAT table version is now set to 1.1, to improve
+ compatibility with legacy applications (#1413).
+
+3.34.2 (released 2018-12-17)
+----------------------------
+
+- [merge] Fixed AssertionError when none of the script tables in GPOS/GSUB have
+ a DefaultLangSys record (#1408, 135a4a1).
+
+3.34.1 (released 2018-12-17)
+----------------------------
+
+- [varLib] Work around macOS rendering issue for composites without gvar entry (#1381).
+
+3.34.0 (released 2018-12-14)
+----------------------------
+
+- [varLib] Support generation of CFF2 variable fonts. ``model.reorderMasters()``
+ now supports arbitrary mapping. Fix handling of overlapping ranges for feature
+ variations (#1400).
+- [cffLib, subset] Code clean-up and fixing related to CFF2 support.
+- [ttLib.tables.ttProgram] Use raw strings for regex patterns (#1389).
+- [fontbuilder] Initial support for building CFF2 fonts. Set CFF's
+ ``FontMatrix`` automatically from unitsPerEm.
+- [plistLib] Accept the more general ``collections.Mapping`` instead of the
+ specific ``dict`` class to support custom data classes that should serialize
+ to dictionaries.
+
+3.33.0 (released 2018-11-30)
+----------------------------
+- [subset] subsetter bug fix with variable fonts.
+- [varLib.featureVar] Improve FeatureVariations generation with many rules.
+- [varLib] Enable sparse masters when building variable fonts:
+ https://github.com/fonttools/fonttools/pull/1368#issuecomment-437257368
+- [varLib.mutator] Add IDEF for GETVARIATION opcode, for handling hints in an
+ instance.
+- [ttLib] Ignore the length of kern table subtable format 0
+
+3.32.0 (released 2018-11-01)
+----------------------------
+
+- [ufoLib] Make ``UFOWriter`` a subclass of ``UFOReader``, and use mixins
+ for shared methods (#1344).
+- [featureVars] Fixed normalization error when a condition's minimum/maximum
+ attributes are missing in designspace ``<rule>`` (#1366).
+- [setup.py] Added ``[plot]`` to extras, to optionally install ``matplotlib``,
+ needed to use the ``fonTools.varLib.plot`` module.
+- [varLib] Take total bounding box into account when resolving model (7ee81c8).
+ If multiple axes have the same range ratio, cut across both (62003f4).
+- [subset] Don't error if ``STAT`` has no ``AxisValue`` tables.
+- [fontBuilder] Added a new submodule which contains a ``FontBuilder`` wrapper
+ class around ``TTFont`` that makes it easier to create a working TTF or OTF
+ font from scratch with code. NOTE: the API is still experimental and may
+ change in future versions.
+
3.31.0 (released 2018-10-21)
----------------------------
diff --git a/PKG-INFO b/PKG-INFO
index ee3a3772..32f95682 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 3.31.0
+Version: 3.35.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -28,6 +28,13 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4
or later.
+ **NOTE** After January 1 2019, until no later than June 30 2019, the support
+ for *Python 2.7* will be limited to only bug fixes, and no new features will
+ be added to the ``py27`` branch. The upcoming FontTools 4.x series will require
+ *Python 3.5* or above. You can read more `here <https://python3statement.org>`__
+ and `here <https://github.com/fonttools/fonttools/issues/765>`__ for the
+ reasons behind this decision.
+
The package is listed in the Python Package Index (PyPI), so you can
install it with `pip <https://pip.pypa.io>`__:
@@ -255,6 +262,14 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
*Extra:* ``interpolatable``
+ - ``Lib/fontTools/varLib/plot.py``
+
+ Module for visualizing DesignSpaceDocument and resulting VariationModel.
+
+ * `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library.
+
+ *Extra:* ``plot``
+
- ``Lib/fontTools/misc/symfont.py``
Advanced module for symbolic font statistics analysis; it requires:
@@ -314,7 +329,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
installed ``fontTools`` package, or the first one found in the
``PYTHONPATH``.
- You can also use `tox <https://testrun.org/tox/latest/>`__ to
+ You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
automatically run tests on different Python versions in isolated virtual
environments.
@@ -415,6 +430,82 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
Changelog
~~~~~~~~~
+ 3.35.0 (released 2019-01-07)
+ ----------------------------
+
+ - [psCharStrings] In ``encodeFloat`` function, use float's "general format" with
+ 8 digits of precision (i.e. ``%8g``) instead of ``str()``. This works around
+ a macOS rendering issue when real numbers in CFF table are too long, and
+ also makes sure that floats are encoded with the same precision in python 2.7
+ and 3.x (#1430, googlei18n/ufo2ft#306).
+ - [_n_a_m_e/fontBuilder] Make ``_n_a_m_e_table.addMultilingualName`` also add
+ Macintosh (platformID=1) names by default. Added options to ``FontBuilder``
+ ``setupNameTable`` method to optionally disable Macintosh or Windows names.
+ (#1359, #1431).
+ - [varLib] Make ``build`` optionally accept a ``DesignSpaceDocument`` object,
+ instead of a designspace file path. The caller can now set the ``font``
+ attribute of designspace's sources to a TTFont object, thus allowing to
+ skip filenames manipulation altogether (#1416, #1425).
+ - [sfnt] Allow SFNTReader objects to be deep-copied.
+ - Require typing>=3.6.4 on py27 to fix issue with singledispatch (#1423).
+ - [designspaceLib/t1Lib/macRes] Fixed some cases where pathlib.Path objects were
+ not accepted (#1421).
+ - [varLib] Fixed merging of multiple PairPosFormat2 subtables (#1411).
+ - [varLib] The default STAT table version is now set to 1.1, to improve
+ compatibility with legacy applications (#1413).
+
+ 3.34.2 (released 2018-12-17)
+ ----------------------------
+
+ - [merge] Fixed AssertionError when none of the script tables in GPOS/GSUB have
+ a DefaultLangSys record (#1408, 135a4a1).
+
+ 3.34.1 (released 2018-12-17)
+ ----------------------------
+
+ - [varLib] Work around macOS rendering issue for composites without gvar entry (#1381).
+
+ 3.34.0 (released 2018-12-14)
+ ----------------------------
+
+ - [varLib] Support generation of CFF2 variable fonts. ``model.reorderMasters()``
+ now supports arbitrary mapping. Fix handling of overlapping ranges for feature
+ variations (#1400).
+ - [cffLib, subset] Code clean-up and fixing related to CFF2 support.
+ - [ttLib.tables.ttProgram] Use raw strings for regex patterns (#1389).
+ - [fontbuilder] Initial support for building CFF2 fonts. Set CFF's
+ ``FontMatrix`` automatically from unitsPerEm.
+ - [plistLib] Accept the more general ``collections.Mapping`` instead of the
+ specific ``dict`` class to support custom data classes that should serialize
+ to dictionaries.
+
+ 3.33.0 (released 2018-11-30)
+ ----------------------------
+ - [subset] subsetter bug fix with variable fonts.
+ - [varLib.featureVar] Improve FeatureVariations generation with many rules.
+ - [varLib] Enable sparse masters when building variable fonts:
+ https://github.com/fonttools/fonttools/pull/1368#issuecomment-437257368
+ - [varLib.mutator] Add IDEF for GETVARIATION opcode, for handling hints in an
+ instance.
+ - [ttLib] Ignore the length of kern table subtable format 0
+
+ 3.32.0 (released 2018-11-01)
+ ----------------------------
+
+ - [ufoLib] Make ``UFOWriter`` a subclass of ``UFOReader``, and use mixins
+ for shared methods (#1344).
+ - [featureVars] Fixed normalization error when a condition's minimum/maximum
+ attributes are missing in designspace ``<rule>`` (#1366).
+ - [setup.py] Added ``[plot]`` to extras, to optionally install ``matplotlib``,
+ needed to use the ``fonTools.varLib.plot`` module.
+ - [varLib] Take total bounding box into account when resolving model (7ee81c8).
+ If multiple axes have the same range ratio, cut across both (62003f4).
+ - [subset] Don't error if ``STAT`` has no ``AxisValue`` tables.
+ - [fontBuilder] Added a new submodule which contains a ``FontBuilder`` wrapper
+ class around ``TTFont`` that makes it easier to create a working TTF or OTF
+ font from scratch with code. NOTE: the API is still experimental and may
+ change in future versions.
+
3.31.0 (released 2018-10-21)
----------------------------
@@ -1491,11 +1582,13 @@ Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
-Provides-Extra: type1
+Provides-Extra: all
+Provides-Extra: interpolatable
+Provides-Extra: woff
Provides-Extra: lxml
Provides-Extra: unicode
-Provides-Extra: symfont
-Provides-Extra: all
+Provides-Extra: graphite
Provides-Extra: ufo
-Provides-Extra: woff
-Provides-Extra: interpolatable
+Provides-Extra: type1
+Provides-Extra: plot
+Provides-Extra: symfont
diff --git a/README.rst b/README.rst
index 5f39ed85..f36084ad 100644
--- a/README.rst
+++ b/README.rst
@@ -18,6 +18,13 @@ Installation
FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4
or later.
+**NOTE** After January 1 2019, until no later than June 30 2019, the support
+for *Python 2.7* will be limited to only bug fixes, and no new features will
+be added to the ``py27`` branch. The upcoming FontTools 4.x series will require
+*Python 3.5* or above. You can read more `here <https://python3statement.org>`__
+and `here <https://github.com/fonttools/fonttools/issues/765>`__ for the
+reasons behind this decision.
+
The package is listed in the Python Package Index (PyPI), so you can
install it with `pip <https://pip.pypa.io>`__:
@@ -245,6 +252,14 @@ are required to unlock the extra features named "ufo", etc.
*Extra:* ``interpolatable``
+- ``Lib/fontTools/varLib/plot.py``
+
+ Module for visualizing DesignSpaceDocument and resulting VariationModel.
+
+ * `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library.
+
+ *Extra:* ``plot``
+
- ``Lib/fontTools/misc/symfont.py``
Advanced module for symbolic font statistics analysis; it requires:
@@ -304,7 +319,7 @@ When you run the ``pytest`` command, the tests will run against the
installed ``fontTools`` package, or the first one found in the
``PYTHONPATH``.
-You can also use `tox <https://testrun.org/tox/latest/>`__ to
+You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
automatically run tests on different Python versions in isolated virtual
environments.
diff --git a/Snippets/cmap-format.py b/Snippets/cmap-format.py
index 0cee39c5..0cee39c5 100755..100644
--- a/Snippets/cmap-format.py
+++ b/Snippets/cmap-format.py
diff --git a/Snippets/interpolate.py b/Snippets/interpolate.py
index 7ed822d2..7ed822d2 100755..100644
--- a/Snippets/interpolate.py
+++ b/Snippets/interpolate.py
diff --git a/Snippets/layout-features.py b/Snippets/layout-features.py
index 25522cda..25522cda 100755..100644
--- a/Snippets/layout-features.py
+++ b/Snippets/layout-features.py
diff --git a/Snippets/otf2ttf.py b/Snippets/otf2ttf.py
index 62b4f735..62b4f735 100755..100644
--- a/Snippets/otf2ttf.py
+++ b/Snippets/otf2ttf.py
diff --git a/Snippets/rename-fonts.py b/Snippets/rename-fonts.py
index ddfce103..ddfce103 100755..100644
--- 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 100755..100644
--- a/Snippets/subset-fpgm.py
+++ b/Snippets/subset-fpgm.py
diff --git a/Snippets/svg2glif.py b/Snippets/svg2glif.py
index 2dd64027..2dd64027 100755..100644
--- a/Snippets/svg2glif.py
+++ b/Snippets/svg2glif.py
diff --git a/Snippets/woff2_compress.py b/Snippets/woff2_compress.py
index 689ebdcc..689ebdcc 100755..100644
--- 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 100755..100644
--- a/Snippets/woff2_decompress.py
+++ b/Snippets/woff2_decompress.py
diff --git a/Tests/designspaceLib/designspace_test.py b/Tests/designspaceLib/designspace_test.py
index b5d0b70d..2044f00b 100644
--- a/Tests/designspaceLib/designspace_test.py
+++ b/Tests/designspaceLib/designspace_test.py
@@ -4,6 +4,7 @@ from __future__ import (print_function, division, absolute_import,
unicode_literals)
import os
+import sys
import pytest
import warnings
@@ -237,12 +238,10 @@ def test_unicodes(tmpdir):
new.read(testDocPath)
new.write(testDocPath2)
# compare the file contents
- f1 = open(testDocPath, 'r', encoding='utf-8')
- t1 = f1.read()
- f1.close()
- f2 = open(testDocPath2, 'r', encoding='utf-8')
- t2 = f2.read()
- f2.close()
+ with open(testDocPath, 'r', encoding='utf-8') as f1:
+ t1 = f1.read()
+ with open(testDocPath2, 'r', encoding='utf-8') as f2:
+ t2 = f2.read()
assert t1 == t2
# check the unicode values read from the document
assert new.instances[0].glyphs['arrow']['unicodes'] == [100,200,300]
@@ -337,12 +336,10 @@ def test_localisedNames(tmpdir):
new = DesignSpaceDocument()
new.read(testDocPath)
new.write(testDocPath2)
- f1 = open(testDocPath, 'r', encoding='utf-8')
- t1 = f1.read()
- f1.close()
- f2 = open(testDocPath2, 'r', encoding='utf-8')
- t2 = f2.read()
- f2.close()
+ with open(testDocPath, 'r', encoding='utf-8') as f1:
+ t1 = f1.read()
+ with open(testDocPath2, 'r', encoding='utf-8') as f2:
+ t2 = f2.read()
assert t1 == t2
@@ -761,14 +758,12 @@ def _addUnwrappedCondition(path):
# only for testing, so we can make an invalid designspace file
# older designspace files may have conditions that are not wrapped in a conditionset
# These can be read into a new conditionset.
- f = open(path, 'r', encoding='utf-8')
- d = f.read()
+ with open(path, 'r', encoding='utf-8') as f:
+ d = f.read()
print(d)
- f.close()
d = d.replace('<rule name="named.rule.1">', '<rule name="named.rule.1">\n\t<condition maximum="22" minimum="33" name="axisName_a" />')
- f = open(path, 'w', encoding='utf-8')
- f.write(d)
- f.close()
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(d)
def test_documentLib(tmpdir):
# roundtrip test of the document lib with some nested data
@@ -791,3 +786,64 @@ def test_documentLib(tmpdir):
assert dummyKey in new.lib
assert new.lib[dummyKey] == dummyData
+
+def test_updatePaths(tmpdir):
+ doc = DesignSpaceDocument()
+ doc.path = str(tmpdir / "foo" / "bar" / "MyDesignspace.designspace")
+
+ s1 = SourceDescriptor()
+ doc.addSource(s1)
+
+ doc.updatePaths()
+
+ # expect no changes
+ assert s1.path is None
+ assert s1.filename is None
+
+ name1 = "../masters/Source1.ufo"
+ path1 = posix(str(tmpdir / "foo" / "masters" / "Source1.ufo"))
+
+ s1.path = path1
+ s1.filename = None
+
+ doc.updatePaths()
+
+ assert s1.path == path1
+ assert s1.filename == name1 # empty filename updated
+
+ name2 = "../masters/Source2.ufo"
+ s1.filename = name2
+
+ doc.updatePaths()
+
+ # conflicting filename discarded, path always gets precedence
+ assert s1.path == path1
+ assert s1.filename == "../masters/Source1.ufo"
+
+ s1.path = None
+ s1.filename = name2
+
+ doc.updatePaths()
+
+ # expect no changes
+ assert s1.path is None
+ assert s1.filename == name2
+
+
+@pytest.mark.skipif(sys.version_info[:2] < (3, 6), reason="pathlib is only tested on 3.6 and up")
+def test_read_with_path_object():
+ import pathlib
+ source = (pathlib.Path(__file__) / "../data/test.designspace").resolve()
+ assert source.exists()
+ doc = DesignSpaceDocument()
+ doc.read(source)
+
+
+@pytest.mark.skipif(sys.version_info[:2] < (3, 6), reason="pathlib is only tested on 3.6 and up")
+def test_with_with_path_object(tmpdir):
+ import pathlib
+ tmpdir = str(tmpdir)
+ dest = pathlib.Path(tmpdir) / "test.designspace"
+ doc = DesignSpaceDocument()
+ doc.write(dest)
+ assert dest.exists()
diff --git a/Tests/fontBuilder/data/test.otf.ttx b/Tests/fontBuilder/data/test.otf.ttx
new file mode 100644
index 00000000..4c9a2a75
--- /dev/null
+++ b/Tests/fontBuilder/data/test.otf.ttx
@@ -0,0 +1,253 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="OTTO" ttLibVersion="3.31">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name=".null"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="a"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x9198bee"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1024"/>
+ <created value="Wed Oct 31 19:40:57 2018"/>
+ <modified value="Wed Oct 31 19:40:57 2018"/>
+ <xMin value="100"/>
+ <yMin value="100"/>
+ <xMax value="500"/>
+ <yMax value="1000"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="3"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="824"/>
+ <descent value="200"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="600"/>
+ <minLeftSideBearing value="100"/>
+ <minRightSideBearing value="100"/>
+ <xMaxExtent value="500"/>
+ <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="1"/>
+ </hhea>
+
+ <maxp>
+ <tableVersion value="0x5000"/>
+ <numGlyphs value="4"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="600"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="0"/>
+ <ySubscriptYSize value="0"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="0"/>
+ <ySuperscriptXSize value="0"/>
+ <ySuperscriptYSize value="0"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="0"/>
+ <yStrikeoutSize value="0"/>
+ <yStrikeoutPosition value="0"/>
+ <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 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="????"/>
+ <fsSelection value="00000000 00000000"/>
+ <usFirstCharIndex value="65"/>
+ <usLastCharIndex value="97"/>
+ <sTypoAscender value="0"/>
+ <sTypoDescender value="0"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="0"/>
+ <usWinDescent value="0"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000000"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="0"/>
+ <sCapHeight value="0"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="2"/>
+ </OS_2>
+
+ <name>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ HelloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ HelloTestFont-TotallyNormal
+ </namerecord>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
+ HalloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x4" unicode="True">
+ TotaalNormaal
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ HelloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ HelloTestFont-TotallyNormal
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
+ HalloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x413">
+ TotaalNormaal
+ </namerecord>
+ </name>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ </cmap>
+
+ <post>
+ <formatType value="3.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="0"/>
+ <underlineThickness value="0"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ </post>
+
+ <CFF>
+ <major value="1"/>
+ <minor value="0"/>
+ <CFFFont name="HelloTestFont-TotallyNormal">
+ <FullName value="HelloTestFont-TotallyNormal"/>
+ <isFixedPitch value="0"/>
+ <ItalicAngle value="0"/>
+ <UnderlinePosition value="-100"/>
+ <UnderlineThickness value="50"/>
+ <PaintType value="0"/>
+ <CharstringType value="2"/>
+ <FontMatrix value="0.0009765625 0 0 0.0009765625 0 0"/>
+ <FontBBox value="100 100 500 1000"/>
+ <StrokeWidth value="0"/>
+ <!-- charset is dumped separately as the 'GlyphOrder' element -->
+ <Encoding name="StandardEncoding"/>
+ <Private>
+ <BlueScale value="0.039625"/>
+ <BlueShift value="7"/>
+ <BlueFuzz value="1"/>
+ <ForceBold value="0"/>
+ <LanguageGroup value="0"/>
+ <ExpansionFactor value="0.06"/>
+ <initialRandomSeed value="0"/>
+ <defaultWidthX value="0"/>
+ <nominalWidthX value="0"/>
+ </Private>
+ <CharStrings>
+ <CharString name=".notdef">
+ 600 100 100 rmoveto
+ 900 vlineto
+ -67 67 66 -33 67 hhcurveto
+ 67 66 33 67 67 hvcurveto
+ -900 vlineto
+ endchar
+ </CharString>
+ <CharString name=".null">
+ 600 100 100 rmoveto
+ 900 vlineto
+ -67 67 66 -33 67 hhcurveto
+ 67 66 33 67 67 hvcurveto
+ -900 vlineto
+ endchar
+ </CharString>
+ <CharString name="A">
+ 600 100 100 rmoveto
+ 900 vlineto
+ -67 67 66 -33 67 hhcurveto
+ 67 66 33 67 67 hvcurveto
+ -900 vlineto
+ endchar
+ </CharString>
+ <CharString name="a">
+ 600 100 100 rmoveto
+ 900 vlineto
+ -67 67 66 -33 67 hhcurveto
+ 67 66 33 67 67 hvcurveto
+ -900 vlineto
+ endchar
+ </CharString>
+ </CharStrings>
+ </CFFFont>
+
+ <GlobalSubrs>
+ <!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
+ </GlobalSubrs>
+ </CFF>
+
+ <hmtx>
+ <mtx name=".notdef" width="600" lsb="100"/>
+ <mtx name=".null" width="600" lsb="100"/>
+ <mtx name="A" width="600" lsb="100"/>
+ <mtx name="a" width="600" lsb="100"/>
+ </hmtx>
+
+ <DSIG>
+ <!-- note that the Digital Signature will be invalid after recompilation! -->
+ <tableHeader flag="0x1" numSigs="1" version="1"/>
+ <SignatureRecord format="1">
+-----BEGIN PKCS7-----
+0000000100000000
+-----END PKCS7-----
+ </SignatureRecord>
+ </DSIG>
+
+</ttFont>
diff --git a/Tests/fontBuilder/data/test.ttf.ttx b/Tests/fontBuilder/data/test.ttf.ttx
new file mode 100644
index 00000000..b2804ccd
--- /dev/null
+++ b/Tests/fontBuilder/data/test.ttf.ttx
@@ -0,0 +1,270 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.31">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name=".null"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="a"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x7adb7b76"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1024"/>
+ <created value="Wed Oct 31 19:40:57 2018"/>
+ <modified value="Wed Oct 31 19:40:57 2018"/>
+ <xMin value="100"/>
+ <yMin value="100"/>
+ <xMax value="500"/>
+ <yMax value="1000"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="3"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="824"/>
+ <descent value="200"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="600"/>
+ <minLeftSideBearing value="100"/>
+ <minRightSideBearing value="100"/>
+ <xMaxExtent value="500"/>
+ <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="1"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="4"/>
+ <maxPoints value="6"/>
+ <maxContours value="1"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="2"/>
+ <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>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="600"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="0"/>
+ <ySubscriptYSize value="0"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="0"/>
+ <ySuperscriptXSize value="0"/>
+ <ySuperscriptYSize value="0"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="0"/>
+ <yStrikeoutSize value="0"/>
+ <yStrikeoutPosition value="0"/>
+ <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 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="????"/>
+ <fsSelection value="00000000 00000000"/>
+ <usFirstCharIndex value="65"/>
+ <usLastCharIndex value="97"/>
+ <sTypoAscender value="0"/>
+ <sTypoDescender value="0"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="0"/>
+ <usWinDescent value="0"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000000"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="0"/>
+ <sCapHeight value="0"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="2"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="600" lsb="100"/>
+ <mtx name=".null" width="600" lsb="100"/>
+ <mtx name="A" width="600" lsb="100"/>
+ <mtx name="a" width="600" lsb="100"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </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="100" yMin="100" xMax="500" yMax="1000">
+ <contour>
+ <pt x="100" y="100" on="1"/>
+ <pt x="100" y="1000" on="1"/>
+ <pt x="200" y="900" on="0"/>
+ <pt x="400" y="900" on="0"/>
+ <pt x="500" y="1000" on="1"/>
+ <pt x="500" y="100" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name=".null" xMin="100" yMin="100" xMax="500" yMax="1000">
+ <contour>
+ <pt x="100" y="100" on="1"/>
+ <pt x="100" y="1000" on="1"/>
+ <pt x="200" y="900" on="0"/>
+ <pt x="400" y="900" on="0"/>
+ <pt x="500" y="1000" on="1"/>
+ <pt x="500" y="100" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="A" xMin="100" yMin="100" xMax="500" yMax="1000">
+ <contour>
+ <pt x="100" y="100" on="1"/>
+ <pt x="100" y="1000" on="1"/>
+ <pt x="200" y="900" on="0"/>
+ <pt x="400" y="900" on="0"/>
+ <pt x="500" y="1000" on="1"/>
+ <pt x="500" y="100" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="a" xMin="100" yMin="100" xMax="500" yMax="1000">
+ <contour>
+ <pt x="100" y="100" on="1"/>
+ <pt x="100" y="1000" on="1"/>
+ <pt x="200" y="900" on="0"/>
+ <pt x="400" y="900" on="0"/>
+ <pt x="500" y="1000" on="1"/>
+ <pt x="500" y="100" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ HelloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ HelloTestFont-TotallyNormal
+ </namerecord>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
+ HalloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x4" unicode="True">
+ TotaalNormaal
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ HelloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ HelloTestFont-TotallyNormal
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
+ HalloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x413">
+ TotaalNormaal
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="0"/>
+ <underlineThickness value="0"/>
+ <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>
+
+ <DSIG>
+ <!-- note that the Digital Signature will be invalid after recompilation! -->
+ <tableHeader flag="0x1" numSigs="1" version="1"/>
+ <SignatureRecord format="1">
+-----BEGIN PKCS7-----
+0000000100000000
+-----END PKCS7-----
+ </SignatureRecord>
+ </DSIG>
+
+</ttFont>
diff --git a/Tests/fontBuilder/data/test_var.otf.ttx b/Tests/fontBuilder/data/test_var.otf.ttx
new file mode 100644
index 00000000..c9465d23
--- /dev/null
+++ b/Tests/fontBuilder/data/test_var.otf.ttx
@@ -0,0 +1,291 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="OTTO" ttLibVersion="3.33">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="glyph00001"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="a"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0xed07360f"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Wed Dec 5 11:55:26 2018"/>
+ <modified value="Wed Dec 5 11:55:26 2018"/>
+ <xMin value="0"/>
+ <yMin value="0"/>
+ <xMax value="0"/>
+ <yMax value="0"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="3"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="824"/>
+ <descent value="200"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="0"/>
+ <minLeftSideBearing value="0"/>
+ <minRightSideBearing value="0"/>
+ <xMaxExtent value="0"/>
+ <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="1"/>
+ </hhea>
+
+ <maxp>
+ <tableVersion value="0x5000"/>
+ <numGlyphs value="4"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="600"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="0"/>
+ <ySubscriptYSize value="0"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="0"/>
+ <ySuperscriptXSize value="0"/>
+ <ySuperscriptYSize value="0"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="0"/>
+ <yStrikeoutSize value="0"/>
+ <yStrikeoutPosition value="0"/>
+ <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 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="????"/>
+ <fsSelection value="00000000 00000000"/>
+ <usFirstCharIndex value="65"/>
+ <usLastCharIndex value="97"/>
+ <sTypoAscender value="825"/>
+ <sTypoDescender value="200"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="824"/>
+ <usWinDescent value="200"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000000"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="0"/>
+ <sCapHeight value="0"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="2"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="600" lsb="0"/>
+ <mtx name="A" width="600" lsb="0"/>
+ <mtx name="a" width="600" lsb="0"/>
+ <mtx name="glyph00001" width="600" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ </cmap>
+
+ <name>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ HelloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ HelloTestFont-TotallyNormal
+ </namerecord>
+ <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Test Axis
+ </namerecord>
+ <namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ TotallyTested
+ </namerecord>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
+ HalloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x4" unicode="True">
+ TotaalNormaal
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ HelloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ HelloTestFont-TotallyNormal
+ </namerecord>
+ <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
+ Test Axis
+ </namerecord>
+ <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
+ TotallyTested
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
+ HalloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x413">
+ TotaalNormaal
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="3.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="0"/>
+ <underlineThickness value="0"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ </post>
+
+ <CFF2>
+ <major value="2"/>
+ <minor value="0"/>
+ <CFFFont name="CFF2Font">
+ <FontMatrix value="0.001 0 0 0.001 0 0"/>
+ <FDArray>
+ <FontDict index="0">
+ <Private>
+ <BlueScale value="0.039625"/>
+ <BlueShift value="7"/>
+ <BlueFuzz value="1"/>
+ </Private>
+ </FontDict>
+ </FDArray>
+ <CharStrings>
+ <CharString name=".notdef">
+ 100 100 rmoveto
+ 900 vlineto
+ -67 67 66 -33 67 hhcurveto
+ 67 66 33 67 67 hvcurveto
+ -900 vlineto
+ </CharString>
+ <CharString name="A">
+ 100 100 rmoveto
+ 900 vlineto
+ -67 67 66 -33 67 hhcurveto
+ 67 66 33 67 67 hvcurveto
+ -900 vlineto
+ </CharString>
+ <CharString name="a">
+ 200 200 -200 -200 2 blend
+ rmoveto
+ 400 400 1 blend
+ hlineto
+ 400 400 1 blend
+ vlineto
+ -400 -400 1 blend
+ hlineto
+ </CharString>
+ <CharString name="glyph00001">
+ 100 100 rmoveto
+ 900 vlineto
+ -67 67 66 -33 67 hhcurveto
+ 67 66 33 67 67 hvcurveto
+ -900 vlineto
+ </CharString>
+ </CharStrings>
+ <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=0 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=1 -->
+ <VarRegionIndex index="0" value="0"/>
+ </VarData>
+ </VarStore>
+ </CFFFont>
+
+ <GlobalSubrs>
+ <!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
+ </GlobalSubrs>
+ </CFF2>
+
+ <fvar>
+
+ <!-- Test Axis -->
+ <Axis>
+ <AxisTag>TEST</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>100.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- TotallyNormal -->
+ <NamedInstance flags="0x0" subfamilyNameID="257">
+ <coord axis="TEST" value="0.0"/>
+ </NamedInstance>
+
+ <!-- TotallyTested -->
+ <NamedInstance flags="0x0" subfamilyNameID="258">
+ <coord axis="TEST" value="100.0"/>
+ </NamedInstance>
+ </fvar>
+
+</ttFont>
diff --git a/Tests/fontBuilder/data/test_var.ttf.ttx b/Tests/fontBuilder/data/test_var.ttf.ttx
new file mode 100644
index 00000000..760e65a5
--- /dev/null
+++ b/Tests/fontBuilder/data/test_var.ttf.ttx
@@ -0,0 +1,376 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.32">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name=".null"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="a"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x18e72247"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1024"/>
+ <created value="Thu Nov 1 20:29:01 2018"/>
+ <modified value="Thu Nov 1 20:29:01 2018"/>
+ <xMin value="100"/>
+ <yMin value="0"/>
+ <xMax value="500"/>
+ <yMax value="400"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="3"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="824"/>
+ <descent value="200"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="600"/>
+ <minLeftSideBearing value="100"/>
+ <minRightSideBearing value="100"/>
+ <xMaxExtent value="500"/>
+ <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="1"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="4"/>
+ <maxPoints value="4"/>
+ <maxContours value="1"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="2"/>
+ <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>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="600"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="0"/>
+ <ySubscriptYSize value="0"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="0"/>
+ <ySuperscriptXSize value="0"/>
+ <ySuperscriptYSize value="0"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="0"/>
+ <yStrikeoutSize value="0"/>
+ <yStrikeoutPosition value="0"/>
+ <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 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="????"/>
+ <fsSelection value="00000000 00000000"/>
+ <usFirstCharIndex value="65"/>
+ <usLastCharIndex value="97"/>
+ <sTypoAscender value="0"/>
+ <sTypoDescender value="0"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="0"/>
+ <usWinDescent value="0"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000000"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="0"/>
+ <sCapHeight value="0"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="2"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="600" lsb="0"/>
+ <mtx name=".null" width="600" lsb="0"/>
+ <mtx name="A" width="600" lsb="100"/>
+ <mtx name="a" width="600" lsb="100"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </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"/><!-- contains no outline data -->
+
+ <TTGlyph name=".null"/><!-- contains no outline data -->
+
+ <TTGlyph name="A" xMin="100" yMin="0" xMax="500" yMax="400">
+ <contour>
+ <pt x="100" y="0" on="1"/>
+ <pt x="100" y="400" on="1"/>
+ <pt x="500" y="400" on="1"/>
+ <pt x="500" y="0" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="a" xMin="100" yMin="0" xMax="500" yMax="400">
+ <contour>
+ <pt x="100" y="0" on="1"/>
+ <pt x="100" y="400" on="1"/>
+ <pt x="500" y="400" on="1"/>
+ <pt x="500" y="0" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ HelloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ HelloTestFont-TotallyNormal
+ </namerecord>
+ <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Left
+ </namerecord>
+ <namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Right
+ </namerecord>
+ <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Up
+ </namerecord>
+ <namerecord nameID="259" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Down
+ </namerecord>
+ <namerecord nameID="260" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Right Up
+ </namerecord>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
+ HalloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x4" unicode="True">
+ TotaalNormaal
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ HelloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ HelloTestFont-TotallyNormal
+ </namerecord>
+ <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
+ Left
+ </namerecord>
+ <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
+ Right
+ </namerecord>
+ <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
+ Up
+ </namerecord>
+ <namerecord nameID="259" platformID="3" platEncID="1" langID="0x409">
+ Down
+ </namerecord>
+ <namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
+ TotallyNormal
+ </namerecord>
+ <namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
+ Right Up
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
+ HalloTestFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x413">
+ TotaalNormaal
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="0"/>
+ <underlineThickness value="0"/>
+ <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>
+
+ <fvar>
+
+ <!-- Left -->
+ <Axis>
+ <AxisTag>LEFT</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>100.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- Right -->
+ <Axis>
+ <AxisTag>RGHT</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>100.0</MaxValue>
+ <AxisNameID>257</AxisNameID>
+ </Axis>
+
+ <!-- Up -->
+ <Axis>
+ <AxisTag>UPPP</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>100.0</MaxValue>
+ <AxisNameID>258</AxisNameID>
+ </Axis>
+
+ <!-- Down -->
+ <Axis>
+ <AxisTag>DOWN</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>100.0</MaxValue>
+ <AxisNameID>259</AxisNameID>
+ </Axis>
+
+ <!-- TotallyNormal -->
+ <NamedInstance flags="0x0" subfamilyNameID="260">
+ <coord axis="LEFT" value="0.0"/>
+ <coord axis="RGHT" value="0.0"/>
+ <coord axis="UPPP" value="0.0"/>
+ <coord axis="DOWN" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Right Up -->
+ <NamedInstance flags="0x0" subfamilyNameID="261">
+ <coord axis="LEFT" value="0.0"/>
+ <coord axis="RGHT" value="100.0"/>
+ <coord axis="UPPP" value="100.0"/>
+ <coord axis="DOWN" value="0.0"/>
+ </NamedInstance>
+ </fvar>
+
+ <gvar>
+ <version value="1"/>
+ <reserved value="0"/>
+ <glyphVariations glyph="a">
+ <tuple>
+ <coord axis="RGHT" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="200" y="0"/>
+ <delta pt="3" x="200" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="LEFT" value="1.0"/>
+ <delta pt="0" x="-200" y="0"/>
+ <delta pt="1" x="-200" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="UPPP" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="200"/>
+ <delta pt="2" x="0" y="200"/>
+ <delta pt="3" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="DOWN" value="1.0"/>
+ <delta pt="0" x="0" y="-200"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="-200"/>
+ </tuple>
+ </glyphVariations>
+ </gvar>
+
+ <DSIG>
+ <!-- note that the Digital Signature will be invalid after recompilation! -->
+ <tableHeader flag="0x1" numSigs="1" version="1"/>
+ <SignatureRecord format="1">
+-----BEGIN PKCS7-----
+0000000100000000
+-----END PKCS7-----
+ </SignatureRecord>
+ </DSIG>
+
+</ttFont>
diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py
new file mode 100644
index 00000000..d23f0f17
--- /dev/null
+++ b/Tests/fontBuilder/fontBuilder_test.py
@@ -0,0 +1,246 @@
+from __future__ import print_function, division, absolute_import
+from __future__ import unicode_literals
+
+import os
+import shutil
+import re
+from fontTools.ttLib import TTFont
+from fontTools.pens.ttGlyphPen import TTGlyphPen
+from fontTools.pens.t2CharStringPen import T2CharStringPen
+from fontTools.fontBuilder import FontBuilder
+from fontTools.ttLib.tables.TupleVariation import TupleVariation
+from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
+from fontTools.misc.psCharStrings import T2CharString
+
+
+def getTestData(fileName, mode="r"):
+ path = os.path.join(os.path.dirname(__file__), "data", fileName)
+ with open(path, mode) as f:
+ return f.read()
+
+
+def strip_VariableItems(string):
+ # ttlib changes with the fontTools version
+ string = re.sub(' ttLibVersion=".*"', '', string)
+ # head table checksum and creation and mod date changes with each save.
+ string = re.sub('<checkSumAdjustment value="[^"]+"/>', '', string)
+ string = re.sub('<modified value="[^"]+"/>', '', string)
+ string = re.sub('<created value="[^"]+"/>', '', string)
+ return string
+
+
+def drawTestGlyph(pen):
+ pen.moveTo((100, 100))
+ pen.lineTo((100, 1000))
+ pen.qCurveTo((200, 900), (400, 900), (500, 1000))
+ pen.lineTo((500, 100))
+ pen.closePath()
+
+
+def _setupFontBuilder(isTTF, unitsPerEm=1024):
+ fb = FontBuilder(unitsPerEm, isTTF=isTTF)
+ fb.setupGlyphOrder([".notdef", ".null", "A", "a"])
+ fb.setupCharacterMap({65: "A", 97: "a"})
+
+ advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}
+
+ familyName = "HelloTestFont"
+ styleName = "TotallyNormal"
+ nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
+ styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
+ nameStrings['psName'] = familyName + "-" + styleName
+
+ return fb, advanceWidths, nameStrings
+
+
+def _verifyOutput(outPath):
+ f = TTFont(outPath)
+ f.saveXML(outPath + ".ttx")
+ with open(outPath + ".ttx") as f:
+ testData = strip_VariableItems(f.read())
+ refData = strip_VariableItems(getTestData(os.path.basename(outPath) + ".ttx"))
+ assert refData == testData
+
+
+def test_build_ttf(tmpdir):
+ outPath = os.path.join(str(tmpdir), "test.ttf")
+
+ fb, advanceWidths, nameStrings = _setupFontBuilder(True)
+
+ pen = TTGlyphPen(None)
+ drawTestGlyph(pen)
+ glyph = pen.glyph()
+ glyphs = {".notdef": glyph, "A": glyph, "a": glyph, ".null": glyph}
+ fb.setupGlyf(glyphs)
+ metrics = {}
+ glyphTable = fb.font["glyf"]
+ for gn, advanceWidth in advanceWidths.items():
+ metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
+ fb.setupHorizontalMetrics(metrics)
+
+ fb.setupHorizontalHeader(ascent=824, descent=200)
+ fb.setupNameTable(nameStrings)
+ fb.setupOS2()
+ fb.setupPost()
+ fb.setupDummyDSIG()
+
+ fb.save(outPath)
+
+ _verifyOutput(outPath)
+
+
+def test_build_otf(tmpdir):
+ outPath = os.path.join(str(tmpdir), "test.otf")
+
+ fb, advanceWidths, nameStrings = _setupFontBuilder(False)
+
+ pen = T2CharStringPen(600, None)
+ drawTestGlyph(pen)
+ charString = pen.getCharString()
+ charStrings = {".notdef": charString, "A": charString, "a": charString, ".null": charString}
+ fb.setupCFF(nameStrings['psName'], {"FullName": nameStrings['psName']}, charStrings, {})
+ metrics = {}
+ for gn, advanceWidth in advanceWidths.items():
+ metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph
+ fb.setupHorizontalMetrics(metrics)
+
+ fb.setupHorizontalHeader(ascent=824, descent=200)
+ fb.setupNameTable(nameStrings)
+ fb.setupOS2()
+ fb.setupPost()
+ fb.setupDummyDSIG()
+
+ fb.save(outPath)
+
+ _verifyOutput(outPath)
+
+
+def test_build_var(tmpdir):
+ outPath = os.path.join(str(tmpdir), "test_var.ttf")
+
+ fb = FontBuilder(1024, isTTF=True)
+ fb.setupGlyphOrder([".notdef", ".null", "A", "a"])
+ fb.setupCharacterMap({65: "A", 97: "a"})
+
+ advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}
+
+ familyName = "HelloTestFont"
+ styleName = "TotallyNormal"
+ nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
+ styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
+ nameStrings['psName'] = familyName + "-" + styleName
+
+ pen = TTGlyphPen(None)
+ pen.moveTo((100, 0))
+ pen.lineTo((100, 400))
+ pen.lineTo((500, 400))
+ pen.lineTo((500, 000))
+ pen.closePath()
+
+ glyph = pen.glyph()
+
+ pen = TTGlyphPen(None)
+ emptyGlyph = pen.glyph()
+
+ glyphs = {".notdef": emptyGlyph, "A": glyph, "a": glyph, ".null": emptyGlyph}
+ fb.setupGlyf(glyphs)
+ metrics = {}
+ glyphTable = fb.font["glyf"]
+ for gn, advanceWidth in advanceWidths.items():
+ metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
+ fb.setupHorizontalMetrics(metrics)
+
+ fb.setupHorizontalHeader(ascent=824, descent=200)
+ fb.setupNameTable(nameStrings)
+
+ axes = [
+ ('LEFT', 0, 0, 100, "Left"),
+ ('RGHT', 0, 0, 100, "Right"),
+ ('UPPP', 0, 0, 100, "Up"),
+ ('DOWN', 0, 0, 100, "Down"),
+ ]
+ instances = [
+ dict(location=dict(LEFT=0, RGHT=0, UPPP=0, DOWN=0), stylename="TotallyNormal"),
+ dict(location=dict(LEFT=0, RGHT=100, UPPP=100, DOWN=0), stylename="Right Up"),
+ ]
+ fb.setupFvar(axes, instances)
+ variations = {}
+ # Four (x, y) pairs and four phantom points:
+ leftDeltas = [(-200, 0), (-200, 0), (0, 0), (0, 0), None, None, None, None]
+ rightDeltas = [(0, 0), (0, 0), (200, 0), (200, 0), None, None, None, None]
+ upDeltas = [(0, 0), (0, 200), (0, 200), (0, 0), None, None, None, None]
+ downDeltas = [(0, -200), (0, 0), (0, 0), (0, -200), None, None, None, None]
+ variations['a'] = [
+ TupleVariation(dict(RGHT=(0, 1, 1)), rightDeltas),
+ TupleVariation(dict(LEFT=(0, 1, 1)), leftDeltas),
+ TupleVariation(dict(UPPP=(0, 1, 1)), upDeltas),
+ TupleVariation(dict(DOWN=(0, 1, 1)), downDeltas),
+ ]
+ fb.setupGvar(variations)
+
+ fb.setupOS2()
+ fb.setupPost()
+ fb.setupDummyDSIG()
+
+ fb.save(outPath)
+
+ _verifyOutput(outPath)
+
+
+def test_build_cff2(tmpdir):
+ outPath = os.path.join(str(tmpdir), "test_var.otf")
+
+ fb, advanceWidths, nameStrings = _setupFontBuilder(False, 1000)
+
+ fb.setupNameTable(nameStrings)
+
+ axes = [
+ ('TEST', 0, 0, 100, "Test Axis"),
+ ]
+ instances = [
+ dict(location=dict(TEST=0), stylename="TotallyNormal"),
+ dict(location=dict(TEST=100), stylename="TotallyTested"),
+ ]
+ fb.setupFvar(axes, instances)
+
+ pen = T2CharStringPen(None, None, CFF2=True)
+ drawTestGlyph(pen)
+ charString = pen.getCharString()
+
+ program = [
+ 200, 200, -200, -200, 2, "blend", "rmoveto",
+ 400, 400, 1, "blend", "hlineto",
+ 400, 400, 1, "blend", "vlineto",
+ -400, -400, 1, "blend", "hlineto"
+ ]
+ charStringVariable = T2CharString(program=program)
+
+ charStrings = {".notdef": charString, "A": charString, "a": charStringVariable, ".null": charString}
+ fb.setupCFF2(charStrings, regions=[{"TEST": (0, 1, 1)}])
+
+ metrics = {gn: (advanceWidth, 0) for gn, advanceWidth in advanceWidths.items()}
+ fb.setupHorizontalMetrics(metrics)
+
+ fb.setupHorizontalHeader(ascent=824, descent=200)
+ fb.setupOS2(sTypoAscender=825, sTypoDescender=200, usWinAscent=824, usWinDescent=200)
+ fb.setupPost()
+
+ fb.save(outPath)
+
+ _verifyOutput(outPath)
+
+
+def test_setupNameTable_no_mac():
+ fb, _, nameStrings = _setupFontBuilder(True)
+ fb.setupNameTable(nameStrings, mac=False)
+
+ assert all(n for n in fb.font["name"].names if n.platformID == 3)
+ assert not any(n for n in fb.font["name"].names if n.platformID == 1)
+
+
+def test_setupNameTable_no_windows():
+ fb, _, nameStrings = _setupFontBuilder(True)
+ fb.setupNameTable(nameStrings, windows=False)
+
+ 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)
diff --git a/Tests/misc/plistlib_test.py b/Tests/misc/plistlib_test.py
index 041f3328..c8665298 100644
--- a/Tests/misc/plistlib_test.py
+++ b/Tests/misc/plistlib_test.py
@@ -14,6 +14,10 @@ from fontTools.ufoLib.plistlib import (
)
import pytest
+try:
+ from collections.abc import Mapping # python >= 3.3
+except ImportError:
+ from collections import Mapping
PY2 = sys.version_info < (3,)
if PY2:
@@ -530,6 +534,25 @@ def test_non_ascii_bytes():
plistlib.dumps("\U0001f40d".encode("utf-8"), use_builtin_types=False)
+class CustomMapping(Mapping):
+ a = {"a": 1, "b": 2}
+
+ def __getitem__(self, key):
+ return self.a[key]
+
+ def __iter__(self):
+ return iter(self.a)
+
+ def __len__(self):
+ return len(self.a)
+
+
+def test_custom_mapping():
+ test_mapping = CustomMapping()
+ data = plistlib.dumps(test_mapping)
+ assert plistlib.loads(data) == {"a": 1, "b": 2}
+
+
if __name__ == "__main__":
import sys
diff --git a/Tests/misc/psCharStrings_test.py b/Tests/misc/psCharStrings_test.py
index 3f35ac85..f69f7481 100644
--- a/Tests/misc/psCharStrings_test.py
+++ b/Tests/misc/psCharStrings_test.py
@@ -1,7 +1,7 @@
from __future__ import print_function, division, absolute_import
from fontTools.cffLib import PrivateDict
from fontTools.cffLib.specializer import stringToProgram
-from fontTools.misc.psCharStrings import T2CharString
+from fontTools.misc.psCharStrings import T2CharString, encodeFloat, read_realNumber
import unittest
@@ -47,6 +47,47 @@ class T2CharStringTest(unittest.TestCase):
cs2.program, [100, 'rmoveto', -50, -150, 200.5, 0, -50, 150,
'rrcurveto'])
+ def test_encodeFloat(self):
+ import sys
+ def hexenc(s):
+ return ' '.join('%02x' % ord(x) for x in s)
+ if sys.version_info[0] >= 3:
+ def hexenc_py3(s):
+ return ' '.join('%02x' % x for x in s)
+ hexenc = hexenc_py3
+
+ testNums = [
+ # value expected result
+ (-9.399999999999999, '1e e9 a4 ff'), # -9.4
+ (9.399999999999999999, '1e 9a 4f'), # 9.4
+ (456.8, '1e 45 6a 8f'), # 456.8
+ (0.0, '1e 0f'), # 0
+ (-0.0, '1e 0f'), # 0
+ (1.0, '1e 1f'), # 1
+ (-1.0, '1e e1 ff'), # -1
+ (98765.37e2, '1e 98 76 53 7f'), # 9876537
+ (1234567890.0, '1e 1a 23 45 67 9b 09 ff'), # 1234567890
+ (9.876537e-4, '1e a0 00 98 76 53 7f'), # 9.876537e-24
+ (9.876537e+4, '1e 98 76 5a 37 ff'), # 9.876537e+24
+ ]
+
+ for sample in testNums:
+ encoded_result = encodeFloat(sample[0])
+
+ # check to see if we got the expected bytes
+ self.assertEqual(hexenc(encoded_result), sample[1])
+
+ # check to see if we get the same value by decoding the data
+ decoded_result = read_realNumber(
+ None,
+ None,
+ encoded_result,
+ 1,
+ )
+ self.assertEqual(decoded_result[0], float('%.8g' % sample[0]))
+ # We limit to 8 digits of precision to match the implementation
+ # of encodeFloat.
+
if __name__ == "__main__":
import sys
diff --git a/Tests/pens/pointPen_test.py b/Tests/pens/pointPen_test.py
new file mode 100644
index 00000000..9c71c5e2
--- /dev/null
+++ b/Tests/pens/pointPen_test.py
@@ -0,0 +1,412 @@
+from __future__ import print_function, division, absolute_import
+from fontTools.misc.py23 import *
+from fontTools.misc.loggingTools import CapturingLogHandler
+import unittest
+
+from fontTools.pens.basePen import AbstractPen
+from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen, \
+ SegmentToPointPen, GuessSmoothPointPen, ReverseContourPointPen
+
+
+class _TestSegmentPen(AbstractPen):
+
+ def __init__(self):
+ self._commands = []
+
+ def __repr__(self):
+ return " ".join(self._commands)
+
+ def moveTo(self, pt):
+ self._commands.append("%s %s moveto" % (pt[0], pt[1]))
+
+ def lineTo(self, pt):
+ self._commands.append("%s %s lineto" % (pt[0], pt[1]))
+
+ def curveTo(self, *pts):
+ pts = ["%s %s" % pt for pt in pts]
+ self._commands.append("%s curveto" % " ".join(pts))
+
+ def qCurveTo(self, *pts):
+ pts = ["%s %s" % pt if pt is not None else "None" for pt in pts]
+ self._commands.append("%s qcurveto" % " ".join(pts))
+
+ def closePath(self):
+ self._commands.append("closepath")
+
+ def endPath(self):
+ self._commands.append("endpath")
+
+ def addComponent(self, glyphName, transformation):
+ self._commands.append("'%s' %s addcomponent" % (glyphName, transformation))
+
+
+def _reprKwargs(kwargs):
+ items = []
+ for key in sorted(kwargs):
+ value = kwargs[key]
+ if isinstance(value, basestring):
+ items.append("%s='%s'" % (key, value))
+ else:
+ items.append("%s=%s" % (key, value))
+ return items
+
+
+class _TestPointPen(AbstractPointPen):
+
+ def __init__(self):
+ self._commands = []
+
+ def __repr__(self):
+ return " ".join(self._commands)
+
+ def beginPath(self, identifier=None, **kwargs):
+ items = []
+ if identifier is not None:
+ items.append("identifier='%s'" % identifier)
+ items.extend(_reprKwargs(kwargs))
+ self._commands.append("beginPath(%s)" % ", ".join(items))
+
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None,
+ identifier=None, **kwargs):
+ items = ["%s" % (pt,)]
+ if segmentType is not None:
+ items.append("segmentType='%s'" % segmentType)
+ if smooth:
+ items.append("smooth=True")
+ if name is not None:
+ items.append("name='%s'" % name)
+ if identifier is not None:
+ items.append("identifier='%s'" % identifier)
+ items.extend(_reprKwargs(kwargs))
+ self._commands.append("addPoint(%s)" % ", ".join(items))
+
+ def endPath(self):
+ self._commands.append("endPath()")
+
+ def addComponent(self, glyphName, transform, identifier=None, **kwargs):
+ items = ["'%s'" % glyphName, "%s" % transform]
+ if identifier is not None:
+ items.append("identifier='%s'" % identifier)
+ items.extend(_reprKwargs(kwargs))
+ self._commands.append("addComponent(%s)" % ", ".join(items))
+
+
+class PointToSegmentPenTest(unittest.TestCase):
+
+ def test_open(self):
+ pen = _TestSegmentPen()
+ ppen = PointToSegmentPen(pen)
+ ppen.beginPath()
+ ppen.addPoint((10, 10), "move")
+ ppen.addPoint((10, 20), "line")
+ ppen.endPath()
+ self.assertEqual("10 10 moveto 10 20 lineto endpath", repr(pen))
+
+ def test_closed(self):
+ pen = _TestSegmentPen()
+ ppen = PointToSegmentPen(pen)
+ ppen.beginPath()
+ ppen.addPoint((10, 10), "line")
+ ppen.addPoint((10, 20), "line")
+ ppen.addPoint((20, 20), "line")
+ ppen.endPath()
+ self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(pen))
+
+ def test_cubic(self):
+ pen = _TestSegmentPen()
+ ppen = PointToSegmentPen(pen)
+ ppen.beginPath()
+ ppen.addPoint((10, 10), "line")
+ ppen.addPoint((10, 20))
+ ppen.addPoint((20, 20))
+ ppen.addPoint((20, 40), "curve")
+ ppen.endPath()
+ self.assertEqual("10 10 moveto 10 20 20 20 20 40 curveto closepath", repr(pen))
+
+ def test_quad(self):
+ pen = _TestSegmentPen()
+ ppen = PointToSegmentPen(pen)
+ ppen.beginPath(identifier='foo')
+ ppen.addPoint((10, 10), "line")
+ ppen.addPoint((10, 40))
+ ppen.addPoint((40, 40))
+ ppen.addPoint((10, 40), "qcurve")
+ ppen.endPath()
+ self.assertEqual("10 10 moveto 10 40 40 40 10 40 qcurveto closepath", repr(pen))
+
+ def test_quad_onlyOffCurvePoints(self):
+ pen = _TestSegmentPen()
+ ppen = PointToSegmentPen(pen)
+ ppen.beginPath()
+ ppen.addPoint((10, 10))
+ ppen.addPoint((10, 40))
+ ppen.addPoint((40, 40))
+ ppen.endPath()
+ self.assertEqual("10 10 10 40 40 40 None qcurveto closepath", repr(pen))
+
+ def test_roundTrip1(self):
+ tpen = _TestPointPen()
+ ppen = PointToSegmentPen(SegmentToPointPen(tpen))
+ ppen.beginPath()
+ ppen.addPoint((10, 10), "line")
+ ppen.addPoint((10, 20))
+ ppen.addPoint((20, 20))
+ ppen.addPoint((20, 40), "curve")
+ ppen.endPath()
+ self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') addPoint((10, 20)) "
+ "addPoint((20, 20)) addPoint((20, 40), segmentType='curve') endPath()",
+ repr(tpen))
+
+
+class TestSegmentToPointPen(unittest.TestCase):
+
+ def test_move(self):
+ tpen = _TestPointPen()
+ pen = SegmentToPointPen(tpen)
+ pen.moveTo((10, 10))
+ pen.endPath()
+ self.assertEqual("beginPath() addPoint((10, 10), segmentType='move') endPath()",
+ repr(tpen))
+
+ def test_poly(self):
+ tpen = _TestPointPen()
+ pen = SegmentToPointPen(tpen)
+ pen.moveTo((10, 10))
+ pen.lineTo((10, 20))
+ pen.lineTo((20, 20))
+ pen.closePath()
+ self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') "
+ "addPoint((10, 20), segmentType='line') "
+ "addPoint((20, 20), segmentType='line') endPath()",
+ repr(tpen))
+
+ def test_cubic(self):
+ tpen = _TestPointPen()
+ pen = SegmentToPointPen(tpen)
+ pen.moveTo((10, 10))
+ pen.curveTo((10, 20), (20, 20), (20, 10))
+ pen.closePath()
+ self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') "
+ "addPoint((10, 20)) addPoint((20, 20)) addPoint((20, 10), "
+ "segmentType='curve') endPath()", repr(tpen))
+
+ def test_quad(self):
+ tpen = _TestPointPen()
+ pen = SegmentToPointPen(tpen)
+ pen.moveTo((10, 10))
+ pen.qCurveTo((10, 20), (20, 20), (20, 10))
+ pen.closePath()
+ self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') "
+ "addPoint((10, 20)) addPoint((20, 20)) "
+ "addPoint((20, 10), segmentType=qcurve) endPath()",
+ repr(tpen))
+
+ def test_quad(self):
+ tpen = _TestPointPen()
+ pen = SegmentToPointPen(tpen)
+ pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None)
+ pen.closePath()
+ self.assertEqual("beginPath() addPoint((10, 20)) addPoint((20, 20)) "
+ "addPoint((20, 10)) addPoint((10, 10)) endPath()",
+ repr(tpen))
+
+ def test_roundTrip1(self):
+ spen = _TestSegmentPen()
+ pen = SegmentToPointPen(PointToSegmentPen(spen))
+ pen.moveTo((10, 10))
+ pen.lineTo((10, 20))
+ pen.lineTo((20, 20))
+ pen.closePath()
+ self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(spen))
+
+ def test_roundTrip2(self):
+ spen = _TestSegmentPen()
+ pen = SegmentToPointPen(PointToSegmentPen(spen))
+ pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None)
+ pen.closePath()
+ pen.addComponent('base', [1, 0, 0, 1, 0, 0])
+ self.assertEqual("10 20 20 20 20 10 10 10 None qcurveto closepath "
+ "'base' [1, 0, 0, 1, 0, 0] addcomponent",
+ repr(spen))
+
+
+class TestGuessSmoothPointPen(unittest.TestCase):
+
+ def test_guessSmooth_exact(self):
+ tpen = _TestPointPen()
+ pen = GuessSmoothPointPen(tpen)
+ pen.beginPath(identifier="foo")
+ pen.addPoint((0, 100), segmentType="curve")
+ pen.addPoint((0, 200))
+ pen.addPoint((400, 200), identifier='bar')
+ pen.addPoint((400, 100), segmentType="curve")
+ pen.addPoint((400, 0))
+ pen.addPoint((0, 0))
+ pen.endPath()
+ self.assertEqual("beginPath(identifier='foo') "
+ "addPoint((0, 100), segmentType='curve', smooth=True) "
+ "addPoint((0, 200)) addPoint((400, 200), identifier='bar') "
+ "addPoint((400, 100), segmentType='curve', smooth=True) "
+ "addPoint((400, 0)) addPoint((0, 0)) endPath()",
+ repr(tpen))
+
+ def test_guessSmooth_almost(self):
+ tpen = _TestPointPen()
+ pen = GuessSmoothPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((0, 100), segmentType="curve")
+ pen.addPoint((1, 200))
+ pen.addPoint((395, 200))
+ pen.addPoint((400, 100), segmentType="curve")
+ pen.addPoint((400, 0))
+ pen.addPoint((0, 0))
+ pen.endPath()
+ self.assertEqual("beginPath() addPoint((0, 100), segmentType='curve', smooth=True) "
+ "addPoint((1, 200)) addPoint((395, 200)) "
+ "addPoint((400, 100), segmentType='curve', smooth=True) "
+ "addPoint((400, 0)) addPoint((0, 0)) endPath()",
+ repr(tpen))
+
+ def test_guessSmooth_tangent(self):
+ tpen = _TestPointPen()
+ pen = GuessSmoothPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((0, 0), segmentType="move")
+ pen.addPoint((0, 100), segmentType="line")
+ pen.addPoint((3, 200))
+ pen.addPoint((300, 200))
+ pen.addPoint((400, 200), segmentType="curve")
+ pen.endPath()
+ self.assertEqual("beginPath() addPoint((0, 0), segmentType='move') "
+ "addPoint((0, 100), segmentType='line', smooth=True) "
+ "addPoint((3, 200)) addPoint((300, 200)) "
+ "addPoint((400, 200), segmentType='curve') endPath()",
+ repr(tpen))
+
+class TestReverseContourPointPen(unittest.TestCase):
+
+ def test_singlePoint(self):
+ tpen = _TestPointPen()
+ pen = ReverseContourPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((0, 0), segmentType="move")
+ pen.endPath()
+ self.assertEqual("beginPath() "
+ "addPoint((0, 0), segmentType='move') "
+ "endPath()",
+ repr(tpen))
+
+ def test_line(self):
+ tpen = _TestPointPen()
+ pen = ReverseContourPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((0, 0), segmentType="move")
+ pen.addPoint((0, 100), segmentType="line")
+ pen.endPath()
+ self.assertEqual("beginPath() "
+ "addPoint((0, 100), segmentType='move') "
+ "addPoint((0, 0), segmentType='line') "
+ "endPath()",
+ repr(tpen))
+
+ def test_triangle(self):
+ tpen = _TestPointPen()
+ pen = ReverseContourPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((0, 0), segmentType="line")
+ pen.addPoint((0, 100), segmentType="line")
+ pen.addPoint((100, 100), segmentType="line")
+ pen.endPath()
+ self.assertEqual("beginPath() "
+ "addPoint((0, 0), segmentType='line') "
+ "addPoint((100, 100), segmentType='line') "
+ "addPoint((0, 100), segmentType='line') "
+ "endPath()",
+ repr(tpen))
+
+ def test_cubicOpen(self):
+ tpen = _TestPointPen()
+ pen = ReverseContourPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((0, 0), segmentType="move")
+ pen.addPoint((0, 100))
+ pen.addPoint((100, 200))
+ pen.addPoint((200, 200), segmentType="curve")
+ pen.endPath()
+ self.assertEqual("beginPath() "
+ "addPoint((200, 200), segmentType='move') "
+ "addPoint((100, 200)) "
+ "addPoint((0, 100)) "
+ "addPoint((0, 0), segmentType='curve') "
+ "endPath()",
+ repr(tpen))
+
+ def test_quadOpen(self):
+ tpen = _TestPointPen()
+ pen = ReverseContourPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((0, 0), segmentType="move")
+ pen.addPoint((0, 100))
+ pen.addPoint((100, 200))
+ pen.addPoint((200, 200), segmentType="qcurve")
+ pen.endPath()
+ self.assertEqual("beginPath() "
+ "addPoint((200, 200), segmentType='move') "
+ "addPoint((100, 200)) "
+ "addPoint((0, 100)) "
+ "addPoint((0, 0), segmentType='qcurve') "
+ "endPath()",
+ repr(tpen))
+
+ def test_cubicClosed(self):
+ tpen = _TestPointPen()
+ pen = ReverseContourPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((0, 0), segmentType="line")
+ pen.addPoint((0, 100))
+ pen.addPoint((100, 200))
+ pen.addPoint((200, 200), segmentType="curve")
+ pen.endPath()
+ self.assertEqual("beginPath() "
+ "addPoint((0, 0), segmentType='curve') "
+ "addPoint((200, 200), segmentType='line') "
+ "addPoint((100, 200)) "
+ "addPoint((0, 100)) "
+ "endPath()",
+ repr(tpen))
+
+ def test_quadClosedOffCurveStart(self):
+ tpen = _TestPointPen()
+ pen = ReverseContourPointPen(tpen)
+ pen.beginPath()
+ pen.addPoint((100, 200))
+ pen.addPoint((200, 200), segmentType="qcurve")
+ pen.addPoint((0, 0), segmentType="line")
+ pen.addPoint((0, 100))
+ pen.endPath()
+ self.assertEqual("beginPath() "
+ "addPoint((100, 200)) "
+ "addPoint((0, 100)) "
+ "addPoint((0, 0), segmentType='qcurve') "
+ "addPoint((200, 200), segmentType='line') "
+ "endPath()",
+ repr(tpen))
+
+ def test_quadNoOnCurve(self):
+ tpen = _TestPointPen()
+ pen = ReverseContourPointPen(tpen)
+ pen.beginPath(identifier='bar')
+ pen.addPoint((0, 0))
+ pen.addPoint((0, 100), identifier='foo', arbitrary='foo')
+ pen.addPoint((100, 200), arbitrary=123)
+ pen.addPoint((200, 200))
+ pen.endPath()
+ pen.addComponent("base", [1, 0, 0, 1, 0, 0], identifier='foo')
+ self.assertEqual("beginPath(identifier='bar') "
+ "addPoint((0, 0)) "
+ "addPoint((200, 200)) "
+ "addPoint((100, 200), arbitrary=123) "
+ "addPoint((0, 100), identifier='foo', arbitrary='foo') "
+ "endPath() "
+ "addComponent('base', [1, 0, 0, 1, 0, 0], identifier='foo')",
+ repr(tpen))
diff --git a/Tests/t1Lib/t1Lib_test.py b/Tests/t1Lib/t1Lib_test.py
index f5e934de..385dad0d 100644
--- a/Tests/t1Lib/t1Lib_test.py
+++ b/Tests/t1Lib/t1Lib_test.py
@@ -2,6 +2,7 @@ from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
import unittest
import os
+import sys
from fontTools import t1Lib
from fontTools.pens.basePen import NullPen
import random
@@ -50,6 +51,11 @@ class ReadWriteTest(unittest.TestCase):
data = self.write(font, 'OTHER', dohex=True)
self.assertEqual(font.getData(), data)
+ @unittest.skipIf(sys.version_info[:2] < (3, 6), "pathlib is only tested on 3.6 and up")
+ def test_read_with_path(self):
+ import pathlib
+ font = t1Lib.T1Font(pathlib.Path(PFB))
+
@staticmethod
def write(font, outtype, dohex=False):
temp = os.path.join(DATADIR, 'temp.' + outtype.lower())
diff --git a/Tests/ttLib/tables/_k_e_r_n_test.py b/Tests/ttLib/tables/_k_e_r_n_test.py
index 8ab1b1cc..b37748ad 100644
--- a/Tests/ttLib/tables/_k_e_r_n_test.py
+++ b/Tests/ttLib/tables/_k_e_r_n_test.py
@@ -5,6 +5,7 @@ from fontTools.ttLib.tables._k_e_r_n import (
KernTable_format_0, KernTable_format_unkown)
from fontTools.misc.textTools import deHexStr
from fontTools.misc.testTools import FakeFont, getXML, parseXML
+import itertools
import pytest
@@ -120,12 +121,32 @@ KERN_VER_1_FMT_UNKNOWN_XML = [
'</kernsubtable>',
]
+KERN_VER_0_FMT_0_OVERFLOWING_DATA = deHexStr(
+ '0000 ' # 0: version=0
+ '0001 ' # 2: nTables=1
+ '0000 ' # 4: version=0 (bogus field, unused)
+ '0274 ' # 6: length=628 (bogus value for 66164 % 0x10000)
+ '00 ' # 8: format=0
+ '01 ' # 9: coverage=1
+ '2B11 ' # 10: nPairs=11025
+ 'C000 ' # 12: searchRange=49152
+ '000D ' # 14: entrySelector=13
+ '4266 ' # 16: rangeShift=16998
+) + deHexStr(' '.join(
+ '%04X %04X %04X' % (a, b, 0)
+ for (a, b) in itertools.product(range(105), repeat=2)
+))
+
@pytest.fixture
def font():
return FakeFont(list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"))
+@pytest.fixture
+def overflowing_font():
+ return FakeFont(["glyph%i" % i for i in range(105)])
+
class KernTableTest(object):
@@ -364,6 +385,36 @@ class KernTable_format_0_Test(object):
assert subtable[("B", "D")] == 1
assert subtable[("B", "glyph65535")] == 2
+ def test_compileOverflowingSubtable(self, overflowing_font):
+ font = overflowing_font
+ kern = newTable("kern")
+ kern.version = 0
+ st = KernTable_format_0(0)
+ kern.kernTables = [st]
+ st.coverage = 1
+ st.tupleIndex = None
+ st.kernTable = {
+ (a, b): 0
+ for (a, b) in itertools.product(
+ font.getGlyphOrder(), repeat=2)
+ }
+ assert len(st.kernTable) == 11025
+ data = kern.compile(font)
+ assert data == KERN_VER_0_FMT_0_OVERFLOWING_DATA
+
+ def test_decompileOverflowingSubtable(self, overflowing_font):
+ font = overflowing_font
+ data = KERN_VER_0_FMT_0_OVERFLOWING_DATA
+ kern = newTable("kern")
+ kern.decompile(data, font)
+
+ st = kern.kernTables[0]
+ assert st.kernTable == {
+ (a, b): 0
+ for (a, b) in itertools.product(
+ font.getGlyphOrder(), repeat=2)
+ }
+
if __name__ == "__main__":
import sys
diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py
index a27e3c1a..fde14bc8 100644
--- a/Tests/ttLib/tables/_n_a_m_e_test.py
+++ b/Tests/ttLib/tables/_n_a_m_e_test.py
@@ -93,12 +93,12 @@ class NameTableTest(unittest.TestCase):
"en": "Width",
"de-CH": "Breite",
"gsw-LI": "Bräiti",
- }, ttFont=font)
+ }, ttFont=font, mac=False)
self.assertEqual(widthID, 256)
xHeightID = nameTable.addMultilingualName({
"en": "X-Height",
"gsw-LI": "X-Hööchi"
- }, ttFont=font)
+ }, ttFont=font, mac=False)
self.assertEqual(xHeightID, 257)
captor.assertRegex("cannot add Windows name in language gsw-LI")
self.assertEqual(names(nameTable), [
diff --git a/Tests/ufoLib/testSupport.py b/Tests/ufoLib/testSupport.py
index 2982ce84..2982ce84 100755..100644
--- a/Tests/ufoLib/testSupport.py
+++ b/Tests/ufoLib/testSupport.py
diff --git a/Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace b/Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace
new file mode 100644
index 00000000..b47227f4
--- /dev/null
+++ b/Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace
@@ -0,0 +1,22 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.0">
+ <axes>
+ <axis tag="slnt" name="Slant" minimum="-15" maximum="0" default="0"/>
+ </axes>
+ <sources>
+ <source filename="master_ufo/TestFamily4-Regular.ufo" name="master_0" familyname="Test Family 4" stylename="Regular">
+ <lib copy="1"/>
+ <groups copy="1"/>
+ <features copy="1"/>
+ <info copy="1"/>
+ <location>
+ <dimension name="Slant" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="master_ufo/TestFamily4-Italic15.ufo" name="master_1" familyname="Test Family 4" stylename="Italic">
+ <location>
+ <dimension name="Slant" xvalue="-15"/>
+ </location>
+ </source>
+ </sources>
+</designspace>
diff --git a/Tests/varLib/data/FeatureVars.designspace b/Tests/varLib/data/FeatureVars.designspace
index d641ba21..9c958a5a 100644
--- a/Tests/varLib/data/FeatureVars.designspace
+++ b/Tests/varLib/data/FeatureVars.designspace
@@ -9,7 +9,7 @@
<rules>
<rule name="dollar-stroke">
<conditionset>
- <condition name="weight" minimum="500" maximum="1000" />
+ <condition name="weight" minimum="500" /> <!-- intentionally omitted maximum -->
</conditionset>
<sub name="uni0024" with="uni0024.nostroke" />
</rule>
diff --git a/Tests/varLib/data/TestCFF2.designspace b/Tests/varLib/data/TestCFF2.designspace
new file mode 100644
index 00000000..92c45baa
--- /dev/null
+++ b/Tests/varLib/data/TestCFF2.designspace
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<designspace format="3">
+ <axes>
+ <axis default="400.0" maximum="900.0" minimum="200.0" name="weight" tag="wght">
+ <map input="200" output="0" /> <!-- ExtraLight -->
+ <map input="300" output="100" /> <!-- Light -->
+ <map input="400" output="368" /> <!-- Regular -->
+ <map input="500" output="486" /> <!-- Medium -->
+ <map input="600" output="600" /> <!-- Semibold -->
+ <map input="700" output="824" /> <!-- Bold -->
+ <map input="900" output="1000" /><!-- Black -->
+ </axis>
+ </axes>
+ <rules>
+ <rule name="named.rule.1">
+ <conditionset>
+ <condition maximum="600" minimum="0" name="weight" />
+ </conditionset>
+ <sub name="dollar" with="dollar.a" />
+ </rule>
+ </rules>
+ <sources>
+ <source filename="master_cff2/TestCFF2_ExtraLight.ufo" name="master_0">
+ <lib copy="1" />
+ <location>
+ <dimension name="weight" xvalue="0" />
+ </location>
+ </source>
+ <source filename="master_cff2/TestCFF2_Regular.ufo" name="master_1">
+ <glyph mute="1" name="T" />
+ <info copy="1" />
+ <location>
+ <dimension name="weight" xvalue="368" />
+ </location>
+ </source>
+ <source filename="master_cff2/TestCFF2_Black.ufo" name="master_2">
+ <location>
+ <dimension name="weight" xvalue="1000" />
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-ExtraLight" stylename="ExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0" />
+ </location>
+ <kerning />
+ <info />
+ </instance>
+ <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Light" stylename="Light">
+ <location>
+ <dimension name="weight" xvalue="100" />
+ </location>
+ <kerning />
+ <info />
+ </instance>
+ <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Regular" stylename="Regular">
+ <location>
+ <dimension name="weight" xvalue="368" />
+ </location>
+ <kerning />
+ <info />
+ </instance>
+ <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Medium" stylename="Medium">
+ <location>
+ <dimension name="weight" xvalue="486" />
+ </location>
+ <kerning />
+ <info />
+ </instance>
+ <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Semibold" stylename="Semibold">
+ <location>
+ <dimension name="weight" xvalue="600" />
+ </location>
+ <kerning />
+ <info />
+ </instance>
+ <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Bold" stylename="Bold">
+ <location>
+ <dimension name="weight" xvalue="824" />
+ </location>
+ <kerning />
+ <info />
+ </instance>
+ <instance familyname="Test CFF2 Roman" postscriptfontname="TestCFF2Roman-Black" stylename="Black">
+ <location>
+ <dimension name="weight" xvalue="1000" />
+ </location>
+ <kerning />
+ <info />
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/varLib/data/TestCFF2VF.otf b/Tests/varLib/data/TestCFF2VF.otf
new file mode 100644
index 00000000..590ad271
--- /dev/null
+++ b/Tests/varLib/data/TestCFF2VF.otf
Binary files differ
diff --git a/Tests/varLib/data/master_cff2/TestCFF2_Black.otf b/Tests/varLib/data/master_cff2/TestCFF2_Black.otf
new file mode 100644
index 00000000..b4249e10
--- /dev/null
+++ b/Tests/varLib/data/master_cff2/TestCFF2_Black.otf
Binary files differ
diff --git a/Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otf b/Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otf
new file mode 100644
index 00000000..4464791a
--- /dev/null
+++ b/Tests/varLib/data/master_cff2/TestCFF2_ExtraLight.otf
Binary files differ
diff --git a/Tests/varLib/data/master_cff2/TestCFF2_Regular.otf b/Tests/varLib/data/master_cff2/TestCFF2_Regular.otf
new file mode 100644
index 00000000..c87342e4
--- /dev/null
+++ b/Tests/varLib/data/master_cff2/TestCFF2_Regular.otf
Binary files differ
diff --git a/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx b/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx
new file mode 100644
index 00000000..5360fe40
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_getvar_ttf/Mutator_Getvar.ttx
@@ -0,0 +1,555 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.10">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="NULL"/>
+ <GlyphID id="2" name="nonmarkingreturn"/>
+ <GlyphID id="3" name="space"/>
+ <GlyphID id="4" name="b"/>
+ <GlyphID id="5" name="q"/>
+ <GlyphID id="6" name="a"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0xe59c28a1"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00001011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Thu Apr 27 12:41:42 2017"/>
+ <modified value="Tue May 2 16:43:12 2017"/>
+ <xMin value="38"/>
+ <yMin value="-152"/>
+ <xMax value="456"/>
+ <yMax value="608"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="9"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="750"/>
+ <descent value="-250"/>
+ <lineGap value="9"/>
+ <advanceWidthMax value="494"/>
+ <minLeftSideBearing value="38"/>
+ <minRightSideBearing value="38"/>
+ <xMaxExtent value="456"/>
+ <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="5"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="7"/>
+ <maxPoints value="20"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="20"/>
+ <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="1"/>
+ <maxComponentDepth value="1"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="347"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="700"/>
+ <ySubscriptYSize value="650"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="140"/>
+ <ySuperscriptXSize value="700"/>
+ <ySuperscriptYSize value="650"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="477"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="250"/>
+ <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 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="LuFo"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="0"/>
+ <usLastCharIndex value="113"/>
+ <sTypoAscender value="750"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="608"/>
+ <usWinDescent value="152"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="456"/>
+ <sCapHeight value="608"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="200" lsb="0"/>
+ <mtx name="NULL" width="0" lsb="0"/>
+ <mtx name="a" width="494" lsb="38"/>
+ <mtx name="b" width="494" lsb="76"/>
+ <mtx name="nonmarkingreturn" width="200" lsb="0"/>
+ <mtx name="q" width="494" lsb="38"/>
+ <mtx name="space" width="200" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x0" name="NULL"/><!-- ???? -->
+ <map code="0xd" name="nonmarkingreturn"/><!-- ???? -->
+ <map code="0x20" name="space"/><!-- SPACE -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x62" name="b"/><!-- LATIN SMALL LETTER B -->
+ <map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q -->
+ </cmap_format_4>
+ <cmap_format_6 platformID="1" platEncID="0" language="0">
+ <map code="0x0" name="NULL"/>
+ <map code="0xd" name="nonmarkingreturn"/>
+ <map code="0x20" name="space"/>
+ <map code="0x61" name="a"/>
+ <map code="0x62" name="b"/>
+ <map code="0x71" name="q"/>
+ </cmap_format_6>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x0" name="NULL"/><!-- ???? -->
+ <map code="0xd" name="nonmarkingreturn"/><!-- ???? -->
+ <map code="0x20" name="space"/><!-- SPACE -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x62" name="b"/><!-- LATIN SMALL LETTER B -->
+ <map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q -->
+ </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"/><!-- contains no outline data -->
+
+ <TTGlyph name="NULL"/><!-- contains no outline data -->
+
+ <TTGlyph name="a" xMin="38" yMin="-12" xMax="418" yMax="468">
+ <contour>
+ <pt x="342" y="0" on="1"/>
+ <pt x="342" y="64" on="1"/>
+ <pt x="264" y="-12" on="1"/>
+ <pt x="190" y="-12" on="1"/>
+ <pt x="38" y="140" on="1"/>
+ <pt x="38" y="316" on="1"/>
+ <pt x="190" y="468" on="1"/>
+ <pt x="266" y="468" on="1"/>
+ <pt x="342" y="392" on="1"/>
+ <pt x="342" y="456" on="1"/>
+ <pt x="418" y="456" on="1"/>
+ <pt x="418" y="0" on="1"/>
+ </contour>
+ <contour>
+ <pt x="266" y="64" on="1"/>
+ <pt x="342" y="140" on="1"/>
+ <pt x="342" y="316" on="1"/>
+ <pt x="266" y="392" on="1"/>
+ <pt x="190" y="392" on="1"/>
+ <pt x="114" y="316" on="1"/>
+ <pt x="114" y="140" on="1"/>
+ <pt x="190" y="64" on="1"/>
+ </contour>
+ <instructions>
+ <assembly>
+ GETVARIATION[]
+ </assembly>
+ </instructions>
+ </TTGlyph>
+
+ <TTGlyph name="b" xMin="76" yMin="-12" xMax="456" yMax="608">
+ <contour>
+ <pt x="228" y="468" on="1"/>
+ <pt x="304" y="468" on="1"/>
+ <pt x="456" y="316" on="1"/>
+ <pt x="456" y="140" on="1"/>
+ <pt x="304" y="-12" on="1"/>
+ <pt x="230" y="-12" on="1"/>
+ <pt x="152" y="64" on="1"/>
+ <pt x="152" y="0" on="1"/>
+ <pt x="76" y="0" on="1"/>
+ <pt x="76" y="608" on="1"/>
+ <pt x="152" y="608" on="1"/>
+ <pt x="152" y="392" on="1"/>
+ </contour>
+ <contour>
+ <pt x="152" y="316" on="1"/>
+ <pt x="152" y="140" on="1"/>
+ <pt x="228" y="64" on="1"/>
+ <pt x="304" y="64" on="1"/>
+ <pt x="380" y="140" on="1"/>
+ <pt x="380" y="316" on="1"/>
+ <pt x="304" y="392" on="1"/>
+ <pt x="228" y="392" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="nonmarkingreturn"/><!-- contains no outline data -->
+
+ <TTGlyph name="q" xMin="38" yMin="-152" xMax="418" yMax="468">
+ <component glyphName="b" x="494" y="456" scale="-0.99994" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="space"/><!-- contains no outline data -->
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ VarFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ VarFont Regular: 2017
+ </namerecord>
+ <namerecord nameID="4" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ VarFont Regular
+ </namerecord>
+ <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ VarFont-Regular
+ </namerecord>
+ <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Width
+ </namerecord>
+ <namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ascender
+ </namerecord>
+ <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Regular
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ VarFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ VarFont Regular: 2017
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ VarFont Regular
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ VarFont-Regular
+ </namerecord>
+ <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
+ Width
+ </namerecord>
+ <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
+ Ascender
+ </namerecord>
+ <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
+ 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="NULL"/>
+ </extraNames>
+ </post>
+
+ <GDEF>
+ <Version value="0x00010003"/>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=2 -->
+ <!-- RegionCount=2 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=0 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=2 -->
+ <VarRegionIndex index="0" value="0"/>
+ <VarRegionIndex index="1" value="1"/>
+ </VarData>
+ </VarStore>
+ </GDEF>
+
+ <HVAR>
+ <Version value="0x00010000"/>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=2 -->
+ <!-- RegionCount=2 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=2 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=2 -->
+ <VarRegionIndex index="0" value="0"/>
+ <VarRegionIndex index="1" value="1"/>
+ <Item index="0" value="[0, -60]"/>
+ <Item index="1" value="[0, 0]"/>
+ </VarData>
+ </VarStore>
+ <AdvWidthMap>
+ <Map glyph=".notdef" outer="0" inner="1"/>
+ <Map glyph="NULL" outer="0" inner="1"/>
+ <Map glyph="a" outer="0" inner="0"/>
+ <Map glyph="b" outer="0" inner="0"/>
+ <Map glyph="q" outer="0" inner="0"/>
+ <Map glyph="nonmarkingreturn" outer="0" inner="1"/>
+ <Map glyph="space" outer="0" inner="1"/>
+ </AdvWidthMap>
+ </HVAR>
+
+ <MVAR>
+ <Version value="0x00010000"/>
+ <Reserved value="0"/>
+ <ValueRecordSize value="0"/>
+ <!-- ValueRecordCount=0 -->
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=2 -->
+ <!-- RegionCount=2 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=0 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=2 -->
+ <VarRegionIndex index="0" value="0"/>
+ <VarRegionIndex index="1" value="1"/>
+ </VarData>
+ </VarStore>
+ </MVAR>
+
+ <fvar>
+
+ <!-- Width -->
+ <Axis>
+ <AxisTag>wdth</AxisTag>
+ <MinValue>60.0</MinValue>
+ <DefaultValue>100.0</DefaultValue>
+ <MaxValue>100.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- Ascender -->
+ <Axis>
+ <AxisTag>ASCN</AxisTag>
+ <MinValue>608.0</MinValue>
+ <DefaultValue>608.0</DefaultValue>
+ <MaxValue>648.0</MaxValue>
+ <AxisNameID>257</AxisNameID>
+ </Axis>
+
+ <!-- Regular -->
+ <NamedInstance subfamilyNameID="258">
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ASCN" value="608.0"/>
+ </NamedInstance>
+ </fvar>
+
+ <gvar>
+ <version value="1"/>
+ <reserved value="0"/>
+ <glyphVariations glyph="b">
+ <tuple>
+ <coord axis="ASCN" value="1.0"/>
+ <delta pt="8" x="0" y="0"/>
+ <delta pt="9" x="0" y="40"/>
+ <delta pt="10" x="0" y="40"/>
+ <delta pt="11" x="0" y="0"/>
+ <delta pt="12" x="0" y="0"/>
+ <delta pt="22" x="0" y="40"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="-20" y="0"/>
+ <delta pt="1" x="-40" y="0"/>
+ <delta pt="2" x="-60" y="0"/>
+ <delta pt="3" x="-60" y="0"/>
+ <delta pt="4" x="-40" y="0"/>
+ <delta pt="5" x="-20" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="11" x="0" y="0"/>
+ <delta pt="12" x="0" y="0"/>
+ <delta pt="17" x="-60" y="0"/>
+ <delta pt="21" x="-60" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="q">
+ <tuple>
+ <coord axis="ASCN" value="1.0"/>
+ <delta pt="4" x="0" y="40"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="-60" y="0"/>
+ <delta pt="2" x="-60" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="a">
+ <tuple>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="-60" y="0"/>
+ <delta pt="1" x="-60" y="0"/>
+ <delta pt="2" x="-40" y="0"/>
+ <delta pt="3" x="-20" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="-20" y="0"/>
+ <delta pt="7" x="-40" y="0"/>
+ <delta pt="8" x="-60" y="0"/>
+ <delta pt="14" x="-60" y="0"/>
+ <delta pt="20" x="0" y="0"/>
+ <delta pt="21" x="-60" y="0"/>
+ <delta pt="22" x="0" y="0"/>
+ <delta pt="23" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ </gvar>
+
+</ttFont>
diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx
new file mode 100644
index 00000000..f7aa15fc
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Italic15.ttx
@@ -0,0 +1,662 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.34">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="N"/>
+ <GlyphID id="2" name="O"/>
+ <GlyphID id="3" name="Odieresis"/>
+ <GlyphID id="4" name="n"/>
+ <GlyphID id="5" name="o"/>
+ <GlyphID id="6" name="odieresis"/>
+ <GlyphID id="7" name="uni0308"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0xc48e411d"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="750"/>
+ <created value="Sat Oct 21 13:31:38 2017"/>
+ <modified value="Mon Dec 17 12:42:07 2018"/>
+ <xMin value="6"/>
+ <yMin value="-150"/>
+ <xMax value="436"/>
+ <yMax value="650"/>
+ <macStyle value="00000000 00000010"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="750"/>
+ <descent value="-150"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="408"/>
+ <minLeftSideBearing value="6"/>
+ <minRightSideBearing value="-333"/>
+ <xMaxExtent value="436"/>
+ <caretSlopeRise value="1000"/>
+ <caretSlopeRun value="268"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="8"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="8"/>
+ <maxPoints value="36"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="64"/>
+ <maxCompositeContours value="4"/>
+ <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="375"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00001000"/>
+ <ySubscriptXSize value="488"/>
+ <ySubscriptYSize value="450"/>
+ <ySubscriptXOffset value="-15"/>
+ <ySubscriptYOffset value="56"/>
+ <ySuperscriptXSize value="488"/>
+ <ySuperscriptYSize value="450"/>
+ <ySuperscriptXOffset value="70"/>
+ <ySuperscriptYOffset value="263"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="210"/>
+ <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 01000011"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="jens"/>
+ <fsSelection value="00000000 00000001"/>
+ <usFirstCharIndex value="78"/>
+ <usLastCharIndex value="776"/>
+ <sTypoAscender value="600"/>
+ <sTypoDescender value="-150"/>
+ <sTypoLineGap value="150"/>
+ <usWinAscent value="700"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="350"/>
+ <sCapHeight value="500"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="1"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="375" lsb="38"/>
+ <mtx name="N" width="408" lsb="11"/>
+ <mtx name="O" width="404" lsb="33"/>
+ <mtx name="Odieresis" width="404" lsb="33"/>
+ <mtx name="n" width="348" lsb="6"/>
+ <mtx name="o" width="342" lsb="22"/>
+ <mtx name="odieresis" width="342" lsb="22"/>
+ <mtx name="uni0308" width="0" lsb="123"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x4e" name="N"/><!-- LATIN CAPITAL LETTER N -->
+ <map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O -->
+ <map code="0x6e" name="n"/><!-- LATIN SMALL LETTER N -->
+ <map code="0x6f" name="o"/><!-- LATIN SMALL LETTER O -->
+ <map code="0xd6" name="Odieresis"/><!-- LATIN CAPITAL LETTER O WITH DIAERESIS -->
+ <map code="0xf6" name="odieresis"/><!-- LATIN SMALL LETTER O WITH DIAERESIS -->
+ <map code="0x308" name="uni0308"/><!-- COMBINING DIAERESIS -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x4e" name="N"/><!-- LATIN CAPITAL LETTER N -->
+ <map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O -->
+ <map code="0x6e" name="n"/><!-- LATIN SMALL LETTER N -->
+ <map code="0x6f" name="o"/><!-- LATIN SMALL LETTER O -->
+ <map code="0xd6" name="Odieresis"/><!-- LATIN CAPITAL LETTER O WITH DIAERESIS -->
+ <map code="0xf6" name="odieresis"/><!-- LATIN SMALL LETTER O WITH DIAERESIS -->
+ <map code="0x308" name="uni0308"/><!-- COMBINING DIAERESIS -->
+ </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="38" yMin="-150" xMax="337" yMax="600">
+ <contour>
+ <pt x="38" y="-150" on="1"/>
+ <pt x="337" y="-150" on="1"/>
+ <pt x="337" y="600" on="1"/>
+ <pt x="38" y="600" on="1"/>
+ </contour>
+ <contour>
+ <pt x="76" y="-112" on="1"/>
+ <pt x="76" y="562" on="1"/>
+ <pt x="299" y="562" on="1"/>
+ <pt x="299" y="-112" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="N" xMin="11" yMin="-4" xMax="436" yMax="504">
+ <contour>
+ <pt x="38" y="-4" on="1"/>
+ <pt x="26" y="-4" on="0"/>
+ <pt x="11" y="16" on="0"/>
+ <pt x="14" y="28" on="1"/>
+ <pt x="136" y="486" on="1"/>
+ <pt x="138" y="495" on="0"/>
+ <pt x="152" y="504" on="0"/>
+ <pt x="159" y="504" on="1"/>
+ <pt x="167" y="504" on="0"/>
+ <pt x="182" y="495" on="0"/>
+ <pt x="184" y="486" on="1"/>
+ <pt x="287" y="116" on="1"/>
+ <pt x="386" y="485" on="1"/>
+ <pt x="388" y="493" on="0"/>
+ <pt x="401" y="504" on="0"/>
+ <pt x="410" y="504" on="1"/>
+ <pt x="425" y="504" on="0"/>
+ <pt x="436" y="480" on="0"/>
+ <pt x="434" y="472" on="1"/>
+ <pt x="312" y="14" on="1"/>
+ <pt x="310" y="5" on="0"/>
+ <pt x="296" y="-4" on="0"/>
+ <pt x="288" y="-4" on="1"/>
+ <pt x="281" y="-4" on="0"/>
+ <pt x="266" y="5" on="0"/>
+ <pt x="264" y="14" on="1"/>
+ <pt x="161" y="384" on="1"/>
+ <pt x="62" y="15" on="1"/>
+ <pt x="60" y="7" on="0"/>
+ <pt x="47" y="-4" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="O" xMin="33" yMin="-10" xMax="412" yMax="510">
+ <contour>
+ <pt x="159" y="-10" on="1"/>
+ <pt x="116" y="-10" on="0"/>
+ <pt x="57" y="33" on="0"/>
+ <pt x="33" y="105" on="0"/>
+ <pt x="44" y="146" on="1"/>
+ <pt x="103" y="366" on="1"/>
+ <pt x="114" y="406" on="0"/>
+ <pt x="169" y="471" on="0"/>
+ <pt x="245" y="510" on="0"/>
+ <pt x="285" y="510" on="1"/>
+ <pt x="328" y="510" on="0"/>
+ <pt x="388" y="467" on="0"/>
+ <pt x="412" y="396" on="0"/>
+ <pt x="401" y="354" on="1"/>
+ <pt x="342" y="133" on="1"/>
+ <pt x="332" y="94" on="0"/>
+ <pt x="275" y="29" on="0"/>
+ <pt x="199" y="-10" on="0"/>
+ </contour>
+ <contour>
+ <pt x="159" y="40" on="1"/>
+ <pt x="188" y="40" on="0"/>
+ <pt x="244" y="69" on="0"/>
+ <pt x="286" y="117" on="0"/>
+ <pt x="294" y="146" on="1"/>
+ <pt x="353" y="366" on="1"/>
+ <pt x="360" y="393" on="0"/>
+ <pt x="347" y="435" on="0"/>
+ <pt x="312" y="460" on="0"/>
+ <pt x="285" y="460" on="1"/>
+ <pt x="256" y="460" on="0"/>
+ <pt x="200" y="431" on="0"/>
+ <pt x="159" y="383" on="0"/>
+ <pt x="151" y="354" on="1"/>
+ <pt x="92" y="133" on="1"/>
+ <pt x="85" y="107" on="0"/>
+ <pt x="98" y="65" on="0"/>
+ <pt x="133" y="40" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="Odieresis" xMin="33" yMin="-10" xMax="425" yMax="650">
+ <component glyphName="O" x="0" y="0" flags="0x204"/>
+ <component glyphName="uni0308" x="92" y="150" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="n" xMin="6" yMin="-4" xMax="327" yMax="350">
+ <contour>
+ <pt x="34" y="-4" on="1"/>
+ <pt x="22" y="-4" on="0"/>
+ <pt x="6" y="15" on="0"/>
+ <pt x="10" y="28" on="1"/>
+ <pt x="90" y="329" on="1"/>
+ <pt x="96" y="350" on="0"/>
+ <pt x="118" y="350" on="1"/>
+ <pt x="238" y="350" on="1"/>
+ <pt x="269" y="350" on="0"/>
+ <pt x="311" y="321" on="0"/>
+ <pt x="327" y="272" on="0"/>
+ <pt x="319" y="242" on="1"/>
+ <pt x="258" y="15" on="1"/>
+ <pt x="256" y="6" on="0"/>
+ <pt x="243" y="-4" on="0"/>
+ <pt x="234" y="-4" on="1"/>
+ <pt x="222" y="-4" on="0"/>
+ <pt x="206" y="15" on="0"/>
+ <pt x="210" y="28" on="1"/>
+ <pt x="271" y="255" on="1"/>
+ <pt x="276" y="275" on="0"/>
+ <pt x="259" y="300" on="0"/>
+ <pt x="238" y="300" on="1"/>
+ <pt x="134" y="300" on="1"/>
+ <pt x="58" y="15" on="1"/>
+ <pt x="56" y="7" on="0"/>
+ <pt x="43" y="-4" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="o" xMin="22" yMin="-10" xMax="322" yMax="360">
+ <contour>
+ <pt x="129" y="-10" on="1"/>
+ <pt x="91" y="-10" on="0"/>
+ <pt x="40" y="25" on="0"/>
+ <pt x="22" y="86" on="0"/>
+ <pt x="32" y="124" on="1"/>
+ <pt x="64" y="240" on="1"/>
+ <pt x="74" y="276" on="0"/>
+ <pt x="119" y="330" on="0"/>
+ <pt x="180" y="360" on="0"/>
+ <pt x="215" y="360" on="1"/>
+ <pt x="253" y="360" on="0"/>
+ <pt x="304" y="325" on="0"/>
+ <pt x="323" y="265" on="0"/>
+ <pt x="312" y="226" on="1"/>
+ <pt x="280" y="110" on="1"/>
+ <pt x="270" y="75" on="0"/>
+ <pt x="225" y="20" on="0"/>
+ <pt x="164" y="-10" on="0"/>
+ </contour>
+ <contour>
+ <pt x="129" y="40" on="1"/>
+ <pt x="165" y="40" on="0"/>
+ <pt x="222" y="86" on="0"/>
+ <pt x="232" y="124" on="1"/>
+ <pt x="264" y="240" on="1"/>
+ <pt x="273" y="273" on="0"/>
+ <pt x="247" y="310" on="0"/>
+ <pt x="215" y="310" on="1"/>
+ <pt x="179" y="310" on="0"/>
+ <pt x="123" y="264" on="0"/>
+ <pt x="112" y="226" on="1"/>
+ <pt x="80" y="110" on="1"/>
+ <pt x="71" y="77" on="0"/>
+ <pt x="97" y="40" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="odieresis" xMin="22" yMin="-10" xMax="354" yMax="500">
+ <component glyphName="o" x="0" y="0" flags="0x204"/>
+ <component glyphName="uni0308" x="21" y="0" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="uni0308" xMin="123" yMin="425" xMax="333" yMax="500">
+ <contour>
+ <pt x="300" y="425" on="1"/>
+ <pt x="288" y="425" on="0"/>
+ <pt x="273" y="445" on="0"/>
+ <pt x="276" y="456" on="1"/>
+ <pt x="282" y="480" on="1"/>
+ <pt x="284" y="489" on="0"/>
+ <pt x="297" y="500" on="0"/>
+ <pt x="306" y="500" on="1"/>
+ <pt x="318" y="500" on="0"/>
+ <pt x="333" y="480" on="0"/>
+ <pt x="330" y="469" on="1"/>
+ <pt x="324" y="445" on="1"/>
+ <pt x="322" y="436" on="0"/>
+ <pt x="309" y="425" on="0"/>
+ </contour>
+ <contour>
+ <pt x="150" y="425" on="1"/>
+ <pt x="138" y="425" on="0"/>
+ <pt x="123" y="445" on="0"/>
+ <pt x="126" y="456" on="1"/>
+ <pt x="132" y="480" on="1"/>
+ <pt x="134" y="489" on="0"/>
+ <pt x="147" y="500" on="0"/>
+ <pt x="156" y="500" on="1"/>
+ <pt x="168" y="500" on="0"/>
+ <pt x="183" y="480" on="0"/>
+ <pt x="180" y="469" on="1"/>
+ <pt x="174" y="445" on="1"/>
+ <pt x="172" y="436" on="0"/>
+ <pt x="159" y="425" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+ Copyright 2017 by Jens Kutilek
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Test Family 4 15
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Italic
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 1.000;jens;TestFamily4-Italic15
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Test Family 4 Italic 15
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 1.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ TestFamily4-Italic15
+ </namerecord>
+ <namerecord nameID="8" platformID="3" platEncID="1" langID="0x409">
+ Jens Kutilek
+ </namerecord>
+ <namerecord nameID="9" platformID="3" platEncID="1" langID="0x409">
+ Jens Kutilek after the ISO 3098 standard
+ </namerecord>
+ <namerecord nameID="11" platformID="3" platEncID="1" langID="0x409">
+ https://www.kutilek.de/
+ </namerecord>
+ <namerecord nameID="12" platformID="3" platEncID="1" langID="0x409">
+ https://www.kutilek.de/
+ </namerecord>
+ <namerecord nameID="16" platformID="3" platEncID="1" langID="0x409">
+ Test Family 4
+ </namerecord>
+ <namerecord nameID="17" platformID="3" platEncID="1" langID="0x409">
+ Italic 15
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="-15.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="dieresiscomb"/>
+ <psName name="uni0308"/>
+ </extraNames>
+ </post>
+
+ <GDEF>
+ <Version value="0x00010002"/>
+ <GlyphClassDef Format="2">
+ <ClassDef glyph="N" class="1"/>
+ <ClassDef glyph="O" class="1"/>
+ <ClassDef glyph="Odieresis" class="1"/>
+ <ClassDef glyph="n" class="1"/>
+ <ClassDef glyph="o" class="1"/>
+ <ClassDef glyph="odieresis" class="1"/>
+ <ClassDef glyph="uni0308" class="3"/>
+ </GlyphClassDef>
+ <MarkGlyphSetsDef>
+ <MarkSetTableFormat value="1"/>
+ <!-- MarkSetCount=1 -->
+ <Coverage index="0" Format="1">
+ <Glyph value="uni0308"/>
+ </Coverage>
+ </MarkGlyphSetsDef>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=2 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=3 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="1"/>
+ <FeatureIndex index="2" value="2"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="1">
+ <ScriptTag value="latn"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=3 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="1"/>
+ <FeatureIndex index="2" value="2"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=1 -->
+ <LangSysRecord index="0">
+ <LangSysTag value="NLD "/>
+ <LangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=3 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="1"/>
+ <FeatureIndex index="2" value="2"/>
+ </LangSys>
+ </LangSysRecord>
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=3 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="cpsp"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="1">
+ <FeatureTag value="mark"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="2">
+ <FeatureTag value="mkmk"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="2"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=3 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SinglePos index="0" Format="1">
+ <Coverage Format="1">
+ <Glyph value="N"/>
+ <Glyph value="O"/>
+ <Glyph value="Odieresis"/>
+ </Coverage>
+ <ValueFormat value="5"/>
+ <Value XPlacement="25" XAdvance="50"/>
+ </SinglePos>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <MarkBasePos index="0" Format="1">
+ <MarkCoverage Format="1">
+ <Glyph value="uni0308"/>
+ </MarkCoverage>
+ <BaseCoverage Format="2">
+ <Glyph value="N"/>
+ <Glyph value="O"/>
+ <Glyph value="Odieresis"/>
+ <Glyph value="n"/>
+ <Glyph value="o"/>
+ <Glyph value="odieresis"/>
+ </BaseCoverage>
+ <!-- ClassCount=1 -->
+ <MarkArray>
+ <!-- MarkCount=1 -->
+ <MarkRecord index="0">
+ <Class value="0"/>
+ <MarkAnchor Format="1">
+ <XCoordinate value="197"/>
+ <YCoordinate value="350"/>
+ </MarkAnchor>
+ </MarkRecord>
+ </MarkArray>
+ <BaseArray>
+ <!-- BaseCount=6 -->
+ <BaseRecord index="0">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="291"/>
+ <YCoordinate value="500"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="1">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="289"/>
+ <YCoordinate value="500"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="2">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="322"/>
+ <YCoordinate value="625"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="3">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="222"/>
+ <YCoordinate value="350"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="4">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="218"/>
+ <YCoordinate value="350"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="5">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="251"/>
+ <YCoordinate value="475"/>
+ </BaseAnchor>
+ </BaseRecord>
+ </BaseArray>
+ </MarkBasePos>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="6"/>
+ <LookupFlag value="16"/>
+ <!-- SubTableCount=1 -->
+ <MarkMarkPos index="0" Format="1">
+ <Mark1Coverage Format="1">
+ <Glyph value="uni0308"/>
+ </Mark1Coverage>
+ <Mark2Coverage Format="1">
+ <Glyph value="uni0308"/>
+ </Mark2Coverage>
+ <!-- ClassCount=1 -->
+ <Mark1Array>
+ <!-- MarkCount=1 -->
+ <MarkRecord index="0">
+ <Class value="0"/>
+ <MarkAnchor Format="1">
+ <XCoordinate value="197"/>
+ <YCoordinate value="350"/>
+ </MarkAnchor>
+ </MarkRecord>
+ </Mark1Array>
+ <Mark2Array>
+ <!-- Mark2Count=1 -->
+ <Mark2Record index="0">
+ <Mark2Anchor index="0" Format="1">
+ <XCoordinate value="230"/>
+ <YCoordinate value="475"/>
+ </Mark2Anchor>
+ </Mark2Record>
+ </Mark2Array>
+ </MarkMarkPos>
+ <MarkFilteringSet value="0"/>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+</ttFont>
diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx
new file mode 100644
index 00000000..2e354b0a
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily4-Regular.ttx
@@ -0,0 +1,656 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.34">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="N"/>
+ <GlyphID id="2" name="O"/>
+ <GlyphID id="3" name="Odieresis"/>
+ <GlyphID id="4" name="n"/>
+ <GlyphID id="5" name="o"/>
+ <GlyphID id="6" name="odieresis"/>
+ <GlyphID id="7" name="uni0308"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x20783e4b"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="750"/>
+ <created value="Sat Oct 21 13:31:38 2017"/>
+ <modified value="Mon Dec 17 12:42:07 2018"/>
+ <xMin value="38"/>
+ <yMin value="-150"/>
+ <xMax value="354"/>
+ <yMax value="650"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="750"/>
+ <descent value="-150"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="408"/>
+ <minLeftSideBearing value="38"/>
+ <minRightSideBearing value="-250"/>
+ <xMaxExtent value="354"/>
+ <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="8"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="8"/>
+ <maxPoints value="36"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="64"/>
+ <maxCompositeContours value="4"/>
+ <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="375"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00001000"/>
+ <ySubscriptXSize value="488"/>
+ <ySubscriptYSize value="450"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="56"/>
+ <ySuperscriptXSize value="488"/>
+ <ySuperscriptYSize value="450"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="263"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="210"/>
+ <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 01000011"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="jens"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="78"/>
+ <usLastCharIndex value="776"/>
+ <sTypoAscender value="600"/>
+ <sTypoDescender value="-150"/>
+ <sTypoLineGap value="150"/>
+ <usWinAscent value="700"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="350"/>
+ <sCapHeight value="500"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="1"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="375" lsb="38"/>
+ <mtx name="N" width="408" lsb="54"/>
+ <mtx name="O" width="404" lsb="52"/>
+ <mtx name="Odieresis" width="404" lsb="52"/>
+ <mtx name="n" width="348" lsb="50"/>
+ <mtx name="o" width="342" lsb="46"/>
+ <mtx name="odieresis" width="342" lsb="46"/>
+ <mtx name="uni0308" width="0" lsb="50"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x4e" name="N"/><!-- LATIN CAPITAL LETTER N -->
+ <map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O -->
+ <map code="0x6e" name="n"/><!-- LATIN SMALL LETTER N -->
+ <map code="0x6f" name="o"/><!-- LATIN SMALL LETTER O -->
+ <map code="0xd6" name="Odieresis"/><!-- LATIN CAPITAL LETTER O WITH DIAERESIS -->
+ <map code="0xf6" name="odieresis"/><!-- LATIN SMALL LETTER O WITH DIAERESIS -->
+ <map code="0x308" name="uni0308"/><!-- COMBINING DIAERESIS -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x4e" name="N"/><!-- LATIN CAPITAL LETTER N -->
+ <map code="0x4f" name="O"/><!-- LATIN CAPITAL LETTER O -->
+ <map code="0x6e" name="n"/><!-- LATIN SMALL LETTER N -->
+ <map code="0x6f" name="o"/><!-- LATIN SMALL LETTER O -->
+ <map code="0xd6" name="Odieresis"/><!-- LATIN CAPITAL LETTER O WITH DIAERESIS -->
+ <map code="0xf6" name="odieresis"/><!-- LATIN SMALL LETTER O WITH DIAERESIS -->
+ <map code="0x308" name="uni0308"/><!-- COMBINING DIAERESIS -->
+ </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="38" yMin="-150" xMax="337" yMax="600">
+ <contour>
+ <pt x="38" y="-150" on="1"/>
+ <pt x="337" y="-150" on="1"/>
+ <pt x="337" y="600" on="1"/>
+ <pt x="38" y="600" on="1"/>
+ </contour>
+ <contour>
+ <pt x="76" y="-112" on="1"/>
+ <pt x="76" y="562" on="1"/>
+ <pt x="299" y="562" on="1"/>
+ <pt x="299" y="-112" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="N" xMin="54" yMin="-4" xMax="354" yMax="504">
+ <contour>
+ <pt x="79" y="-4" on="1"/>
+ <pt x="69" y="-4" on="0"/>
+ <pt x="54" y="11" on="0"/>
+ <pt x="54" y="21" on="1"/>
+ <pt x="54" y="479" on="1"/>
+ <pt x="54" y="490" on="0"/>
+ <pt x="69" y="504" on="0"/>
+ <pt x="79" y="504" on="1"/>
+ <pt x="88" y="504" on="0"/>
+ <pt x="98" y="496" on="0"/>
+ <pt x="101" y="491" on="1"/>
+ <pt x="304" y="119" on="1"/>
+ <pt x="304" y="479" on="1"/>
+ <pt x="304" y="490" on="0"/>
+ <pt x="319" y="504" on="0"/>
+ <pt x="329" y="504" on="1"/>
+ <pt x="340" y="504" on="0"/>
+ <pt x="354" y="490" on="0"/>
+ <pt x="354" y="479" on="1"/>
+ <pt x="354" y="21" on="1"/>
+ <pt x="354" y="11" on="0"/>
+ <pt x="340" y="-4" on="0"/>
+ <pt x="329" y="-4" on="1"/>
+ <pt x="320" y="-4" on="0"/>
+ <pt x="310" y="4" on="0"/>
+ <pt x="307" y="9" on="1"/>
+ <pt x="104" y="381" on="1"/>
+ <pt x="104" y="21" on="1"/>
+ <pt x="104" y="11" on="0"/>
+ <pt x="90" y="-4" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="O" xMin="52" yMin="-10" xMax="352" yMax="510">
+ <contour>
+ <pt x="202" y="-10" on="1"/>
+ <pt x="161" y="-10" on="0"/>
+ <pt x="93" y="31" on="0"/>
+ <pt x="52" y="99" on="0"/>
+ <pt x="52" y="140" on="1"/>
+ <pt x="52" y="360" on="1"/>
+ <pt x="52" y="401" on="0"/>
+ <pt x="93" y="469" on="0"/>
+ <pt x="161" y="510" on="0"/>
+ <pt x="202" y="510" on="1"/>
+ <pt x="243" y="510" on="0"/>
+ <pt x="311" y="469" on="0"/>
+ <pt x="352" y="401" on="0"/>
+ <pt x="352" y="360" on="1"/>
+ <pt x="352" y="140" on="1"/>
+ <pt x="352" y="99" on="0"/>
+ <pt x="311" y="31" on="0"/>
+ <pt x="243" y="-10" on="0"/>
+ </contour>
+ <contour>
+ <pt x="202" y="40" on="1"/>
+ <pt x="230" y="40" on="0"/>
+ <pt x="275" y="67" on="0"/>
+ <pt x="302" y="113" on="0"/>
+ <pt x="302" y="140" on="1"/>
+ <pt x="302" y="360" on="1"/>
+ <pt x="302" y="388" on="0"/>
+ <pt x="275" y="433" on="0"/>
+ <pt x="230" y="460" on="0"/>
+ <pt x="202" y="460" on="1"/>
+ <pt x="175" y="460" on="0"/>
+ <pt x="129" y="433" on="0"/>
+ <pt x="102" y="388" on="0"/>
+ <pt x="102" y="360" on="1"/>
+ <pt x="102" y="140" on="1"/>
+ <pt x="102" y="113" on="0"/>
+ <pt x="129" y="67" on="0"/>
+ <pt x="175" y="40" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="Odieresis" xMin="52" yMin="-10" xMax="352" yMax="650">
+ <component glyphName="O" x="0" y="0" flags="0x204"/>
+ <component glyphName="uni0308" x="52" y="150" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="n" xMin="50" yMin="-4" xMax="300" yMax="350">
+ <contour>
+ <pt x="75" y="-4" on="1"/>
+ <pt x="65" y="-4" on="0"/>
+ <pt x="50" y="11" on="0"/>
+ <pt x="50" y="21" on="1"/>
+ <pt x="50" y="325" on="1"/>
+ <pt x="50" y="350" on="0"/>
+ <pt x="75" y="350" on="1"/>
+ <pt x="198" y="350" on="1"/>
+ <pt x="228" y="350" on="0"/>
+ <pt x="274" y="324" on="0"/>
+ <pt x="300" y="278" on="0"/>
+ <pt x="300" y="248" on="1"/>
+ <pt x="300" y="21" on="1"/>
+ <pt x="300" y="11" on="0"/>
+ <pt x="286" y="-4" on="0"/>
+ <pt x="275" y="-4" on="1"/>
+ <pt x="265" y="-4" on="0"/>
+ <pt x="250" y="11" on="0"/>
+ <pt x="250" y="21" on="1"/>
+ <pt x="250" y="248" on="1"/>
+ <pt x="250" y="271" on="0"/>
+ <pt x="221" y="300" on="0"/>
+ <pt x="198" y="300" on="1"/>
+ <pt x="100" y="300" on="1"/>
+ <pt x="100" y="21" on="1"/>
+ <pt x="100" y="11" on="0"/>
+ <pt x="86" y="-4" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="o" xMin="46" yMin="-10" xMax="296" yMax="360">
+ <contour>
+ <pt x="171" y="-10" on="1"/>
+ <pt x="135" y="-10" on="0"/>
+ <pt x="78" y="23" on="0"/>
+ <pt x="46" y="80" on="0"/>
+ <pt x="46" y="117" on="1"/>
+ <pt x="46" y="233" on="1"/>
+ <pt x="46" y="270" on="0"/>
+ <pt x="78" y="327" on="0"/>
+ <pt x="135" y="360" on="0"/>
+ <pt x="171" y="360" on="1"/>
+ <pt x="208" y="360" on="0"/>
+ <pt x="264" y="327" on="0"/>
+ <pt x="296" y="270" on="0"/>
+ <pt x="296" y="233" on="1"/>
+ <pt x="296" y="117" on="1"/>
+ <pt x="296" y="80" on="0"/>
+ <pt x="264" y="23" on="0"/>
+ <pt x="208" y="-10" on="0"/>
+ </contour>
+ <contour>
+ <pt x="171" y="40" on="1"/>
+ <pt x="205" y="40" on="0"/>
+ <pt x="246" y="82" on="0"/>
+ <pt x="246" y="117" on="1"/>
+ <pt x="246" y="233" on="1"/>
+ <pt x="246" y="268" on="0"/>
+ <pt x="205" y="310" on="0"/>
+ <pt x="171" y="310" on="1"/>
+ <pt x="137" y="310" on="0"/>
+ <pt x="96" y="268" on="0"/>
+ <pt x="96" y="233" on="1"/>
+ <pt x="96" y="117" on="1"/>
+ <pt x="96" y="82" on="0"/>
+ <pt x="137" y="40" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="odieresis" xMin="46" yMin="-10" xMax="296" yMax="500">
+ <component glyphName="o" x="0" y="0" flags="0x204"/>
+ <component glyphName="uni0308" x="21" y="0" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="uni0308" xMin="50" yMin="425" xMax="250" yMax="500">
+ <contour>
+ <pt x="225" y="425" on="1"/>
+ <pt x="215" y="425" on="0"/>
+ <pt x="200" y="440" on="0"/>
+ <pt x="200" y="450" on="1"/>
+ <pt x="200" y="475" on="1"/>
+ <pt x="200" y="486" on="0"/>
+ <pt x="215" y="500" on="0"/>
+ <pt x="225" y="500" on="1"/>
+ <pt x="236" y="500" on="0"/>
+ <pt x="250" y="486" on="0"/>
+ <pt x="250" y="475" on="1"/>
+ <pt x="250" y="450" on="1"/>
+ <pt x="250" y="440" on="0"/>
+ <pt x="236" y="425" on="0"/>
+ </contour>
+ <contour>
+ <pt x="75" y="425" on="1"/>
+ <pt x="65" y="425" on="0"/>
+ <pt x="50" y="440" on="0"/>
+ <pt x="50" y="450" on="1"/>
+ <pt x="50" y="475" on="1"/>
+ <pt x="50" y="486" on="0"/>
+ <pt x="65" y="500" on="0"/>
+ <pt x="75" y="500" on="1"/>
+ <pt x="86" y="500" on="0"/>
+ <pt x="100" y="486" on="0"/>
+ <pt x="100" y="475" on="1"/>
+ <pt x="100" y="450" on="1"/>
+ <pt x="100" y="440" on="0"/>
+ <pt x="86" y="425" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+ Copyright 2017 by Jens Kutilek
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Test Family 4
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 1.000;jens;TestFamily4-Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Test Family 4 Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 1.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ TestFamily4-Regular
+ </namerecord>
+ <namerecord nameID="8" platformID="3" platEncID="1" langID="0x409">
+ Jens Kutilek
+ </namerecord>
+ <namerecord nameID="9" platformID="3" platEncID="1" langID="0x409">
+ Jens Kutilek after the ISO 3098 standard
+ </namerecord>
+ <namerecord nameID="11" platformID="3" platEncID="1" langID="0x409">
+ https://www.kutilek.de/
+ </namerecord>
+ <namerecord nameID="12" platformID="3" platEncID="1" langID="0x409">
+ https://www.kutilek.de/
+ </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="dieresiscomb"/>
+ <psName name="uni0308"/>
+ </extraNames>
+ </post>
+
+ <GDEF>
+ <Version value="0x00010002"/>
+ <GlyphClassDef Format="2">
+ <ClassDef glyph="N" class="1"/>
+ <ClassDef glyph="O" class="1"/>
+ <ClassDef glyph="Odieresis" class="1"/>
+ <ClassDef glyph="n" class="1"/>
+ <ClassDef glyph="o" class="1"/>
+ <ClassDef glyph="odieresis" class="1"/>
+ <ClassDef glyph="uni0308" class="3"/>
+ </GlyphClassDef>
+ <MarkGlyphSetsDef>
+ <MarkSetTableFormat value="1"/>
+ <!-- MarkSetCount=1 -->
+ <Coverage index="0" Format="1">
+ <Glyph value="uni0308"/>
+ </Coverage>
+ </MarkGlyphSetsDef>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=2 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=3 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="1"/>
+ <FeatureIndex index="2" value="2"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="1">
+ <ScriptTag value="latn"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=3 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="1"/>
+ <FeatureIndex index="2" value="2"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=1 -->
+ <LangSysRecord index="0">
+ <LangSysTag value="NLD "/>
+ <LangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=3 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="1"/>
+ <FeatureIndex index="2" value="2"/>
+ </LangSys>
+ </LangSysRecord>
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=3 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="cpsp"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="1">
+ <FeatureTag value="mark"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="2">
+ <FeatureTag value="mkmk"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="2"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=3 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SinglePos index="0" Format="1">
+ <Coverage Format="1">
+ <Glyph value="N"/>
+ <Glyph value="O"/>
+ <Glyph value="Odieresis"/>
+ </Coverage>
+ <ValueFormat value="5"/>
+ <Value XPlacement="25" XAdvance="50"/>
+ </SinglePos>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="4"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <MarkBasePos index="0" Format="1">
+ <MarkCoverage Format="1">
+ <Glyph value="uni0308"/>
+ </MarkCoverage>
+ <BaseCoverage Format="2">
+ <Glyph value="N"/>
+ <Glyph value="O"/>
+ <Glyph value="Odieresis"/>
+ <Glyph value="n"/>
+ <Glyph value="o"/>
+ <Glyph value="odieresis"/>
+ </BaseCoverage>
+ <!-- ClassCount=1 -->
+ <MarkArray>
+ <!-- MarkCount=1 -->
+ <MarkRecord index="0">
+ <Class value="0"/>
+ <MarkAnchor Format="1">
+ <XCoordinate value="150"/>
+ <YCoordinate value="350"/>
+ </MarkAnchor>
+ </MarkRecord>
+ </MarkArray>
+ <BaseArray>
+ <!-- BaseCount=6 -->
+ <BaseRecord index="0">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="204"/>
+ <YCoordinate value="500"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="1">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="202"/>
+ <YCoordinate value="500"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="2">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="202"/>
+ <YCoordinate value="625"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="3">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="175"/>
+ <YCoordinate value="350"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="4">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="171"/>
+ <YCoordinate value="350"/>
+ </BaseAnchor>
+ </BaseRecord>
+ <BaseRecord index="5">
+ <BaseAnchor index="0" Format="1">
+ <XCoordinate value="171"/>
+ <YCoordinate value="475"/>
+ </BaseAnchor>
+ </BaseRecord>
+ </BaseArray>
+ </MarkBasePos>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="6"/>
+ <LookupFlag value="16"/>
+ <!-- SubTableCount=1 -->
+ <MarkMarkPos index="0" Format="1">
+ <Mark1Coverage Format="1">
+ <Glyph value="uni0308"/>
+ </Mark1Coverage>
+ <Mark2Coverage Format="1">
+ <Glyph value="uni0308"/>
+ </Mark2Coverage>
+ <!-- ClassCount=1 -->
+ <Mark1Array>
+ <!-- MarkCount=1 -->
+ <MarkRecord index="0">
+ <Class value="0"/>
+ <MarkAnchor Format="1">
+ <XCoordinate value="150"/>
+ <YCoordinate value="350"/>
+ </MarkAnchor>
+ </MarkRecord>
+ </Mark1Array>
+ <Mark2Array>
+ <!-- Mark2Count=1 -->
+ <Mark2Record index="0">
+ <Mark2Anchor index="0" Format="1">
+ <XCoordinate value="150"/>
+ <YCoordinate value="475"/>
+ </Mark2Anchor>
+ </Mark2Record>
+ </Mark2Array>
+ </MarkMarkPos>
+ <MarkFilteringSet value="0"/>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+</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 100755..100644
--- 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/master_ufo/TestFamily4-Italic15.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/features.fea
new file mode 100644
index 00000000..88e4c41a
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/features.fea
@@ -0,0 +1,23 @@
+# automatic
+@Uppercase = [ N O Odieresis ];
+
+# Prefix: Languagesystems
+# automatic
+languagesystem DFLT dflt;
+languagesystem latn dflt;
+languagesystem latn NLD;
+
+
+feature cpsp {
+pos @Uppercase <25 0 50 0>;
+
+} cpsp;
+
+table GDEF {
+ # automatic
+ GlyphClassDef
+ [N O Odieresis n o odieresis], # Base
+ , # Liga
+ [dieresiscomb], # Mark
+ ;
+} GDEF;
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist
new file mode 100644
index 00000000..b909a16c
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/fontinfo.plist
@@ -0,0 +1,93 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>ascender</key>
+ <real>600.0</real>
+ <key>capHeight</key>
+ <real>500.0</real>
+ <key>copyright</key>
+ <string>Copyright 2017 by Jens Kutilek</string>
+ <key>descender</key>
+ <real>-150.0</real>
+ <key>familyName</key>
+ <string>Test Family 4</string>
+ <key>guidelines</key>
+ <array>
+ <dict>
+ <key>angle</key>
+ <real>180.0</real>
+ <key>x</key>
+ <real>125.0</real>
+ <key>y</key>
+ <real>175.0</real>
+ </dict>
+ </array>
+ <key>italicAngle</key>
+ <real>-15.0</real>
+ <key>openTypeHeadCreated</key>
+ <string>2017/10/21 13:31:38</string>
+ <key>openTypeNameDesigner</key>
+ <string>Jens Kutilek after the ISO 3098 standard</string>
+ <key>openTypeNameDesignerURL</key>
+ <string>https://www.kutilek.de/</string>
+ <key>openTypeNameManufacturer</key>
+ <string>Jens Kutilek</string>
+ <key>openTypeNameManufacturerURL</key>
+ <string>https://www.kutilek.de/</string>
+ <key>openTypeOS2Type</key>
+ <array>
+ <integer>3</integer>
+ </array>
+ <key>openTypeOS2VendorID</key>
+ <string>jens</string>
+ <key>openTypeOS2WinAscent</key>
+ <integer>700</integer>
+ <key>openTypeOS2WinDescent</key>
+ <integer>250</integer>
+ <key>postscriptBlueValues</key>
+ <array>
+ <real>-10.0</real>
+ <real>0.0</real>
+ <real>350.0</real>
+ <real>360.0</real>
+ <real>500.0</real>
+ <real>510.0</real>
+ </array>
+ <key>postscriptFamilyBlues</key>
+ <array/>
+ <key>postscriptFamilyOtherBlues</key>
+ <array/>
+ <key>postscriptOtherBlues</key>
+ <array>
+ <real>-160.0</real>
+ <real>-150.0</real>
+ </array>
+ <key>postscriptStemSnapH</key>
+ <array>
+ <integer>50</integer>
+ </array>
+ <key>postscriptStemSnapV</key>
+ <array>
+ <integer>50</integer>
+ </array>
+ <key>postscriptUnderlinePosition</key>
+ <integer>-100</integer>
+ <key>postscriptUnderlineThickness</key>
+ <integer>50</integer>
+ <key>styleMapFamilyName</key>
+ <string>Test Family 4 15</string>
+ <key>styleMapStyleName</key>
+ <string>italic</string>
+ <key>styleName</key>
+ <string>Italic 15</string>
+ <key>unitsPerEm</key>
+ <integer>750</integer>
+ <key>versionMajor</key>
+ <integer>1</integer>
+ <key>versionMinor</key>
+ <integer>0</integer>
+ <key>xHeight</key>
+ <real>350.0</real>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif
new file mode 100644
index 00000000..ef809034
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/N_.glif
@@ -0,0 +1,41 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="N" format="2">
+ <anchor x="153.0" y="0.0" name="bottom"/>
+ <anchor x="287.0" y="500.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="34.0" y="21.0" type="move"/>
+ <point x="156.0" y="479.0" type="line"/>
+ <point x="284.0" y="21.0" type="line"/>
+ <point x="406.0" y="479.0" type="line"/>
+ </contour>
+ <contour>
+ <point x="156.0" y="454.0" type="curve" smooth="yes"/>
+ <point x="170.0" y="454.0"/>
+ <point x="181.0" y="465.0"/>
+ <point x="181.0" y="479.0" type="curve" smooth="yes"/>
+ <point x="181.0" y="493.0"/>
+ <point x="170.0" y="504.0"/>
+ <point x="156.0" y="504.0" type="curve" smooth="yes"/>
+ <point x="142.0" y="504.0"/>
+ <point x="131.0" y="493.0"/>
+ <point x="131.0" y="479.0" type="curve" smooth="yes"/>
+ <point x="131.0" y="465.0"/>
+ <point x="142.0" y="454.0"/>
+ </contour>
+ <contour>
+ <point x="284.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="298.0" y="-4.0"/>
+ <point x="309.0" y="7.0"/>
+ <point x="309.0" y="21.0" type="curve" smooth="yes"/>
+ <point x="309.0" y="35.0"/>
+ <point x="298.0" y="46.0"/>
+ <point x="284.0" y="46.0" type="curve" smooth="yes"/>
+ <point x="270.0" y="46.0"/>
+ <point x="259.0" y="35.0"/>
+ <point x="259.0" y="21.0" type="curve" smooth="yes"/>
+ <point x="259.0" y="7.0"/>
+ <point x="270.0" y="-4.0"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif
new file mode 100644
index 00000000..ac3160a6
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/O_.glif
@@ -0,0 +1,27 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="O" format="2">
+ <anchor x="153.0" y="0.0" name="bottom"/>
+ <anchor x="220.0" y="250.0" name="center"/>
+ <anchor x="316.0" y="10.0" name="ogonek"/>
+ <anchor x="287.0" y="500.0" name="top"/>
+ <anchor x="107.0" y="500.0" name="topleft"/>
+ <anchor x="467.0" y="500.0" name="topright"/>
+ <outline>
+ <contour>
+ <point x="66.0" y="140.0" type="curve"/>
+ <point x="125.0" y="360.0" type="line"/>
+ <point x="143.0" y="429.0"/>
+ <point x="214.0" y="485.0"/>
+ <point x="283.0" y="485.0" type="curve"/>
+ <point x="352.0" y="485.0"/>
+ <point x="393.0" y="429.0"/>
+ <point x="375.0" y="360.0" type="curve"/>
+ <point x="316.0" y="140.0" type="line"/>
+ <point x="297.0" y="71.0"/>
+ <point x="226.0" y="15.0"/>
+ <point x="157.0" y="15.0" type="curve"/>
+ <point x="88.0" y="15.0"/>
+ <point x="47.0" y="71.0"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist
new file mode 100644
index 00000000..e7bf8356
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/contents.plist
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>N</key>
+ <string>N_.glif</string>
+ <key>O</key>
+ <string>O_.glif</string>
+ <key>dieresiscomb</key>
+ <string>dieresiscomb.glif</string>
+ <key>o</key>
+ <string>o.glif</string>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif
new file mode 100644
index 00000000..8dac7b72
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/dieresiscomb.glif
@@ -0,0 +1,15 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="dieresiscomb" format="2">
+ <anchor x="197.0" y="350.0" name="_top"/>
+ <anchor x="230.0" y="475.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="155.0" y="475.0" type="move"/>
+ <point x="149.0" y="450.0" type="line"/>
+ </contour>
+ <contour>
+ <point x="305.0" y="475.0" type="move"/>
+ <point x="299.0" y="450.0" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif
new file mode 100644
index 00000000..6a2ce9fd
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs.public.background/o.glif
@@ -0,0 +1,37 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="o" format="2">
+ <outline>
+ <contour>
+ <point x="128.0" y="-10.0" type="curve" smooth="yes"/>
+ <point x="194.0" y="-10.0"/>
+ <point x="262.0" y="43.0"/>
+ <point x="279.0" y="108.0" type="curve" smooth="yes"/>
+ <point x="311.0" y="229.0" type="line" smooth="yes"/>
+ <point x="330.0" y="299.0"/>
+ <point x="286.0" y="360.0"/>
+ <point x="214.0" y="360.0" type="curve" smooth="yes"/>
+ <point x="148.0" y="360.0"/>
+ <point x="80.0" y="307.0"/>
+ <point x="63.0" y="242.0" type="curve" smooth="yes"/>
+ <point x="31.0" y="121.0" type="line" smooth="yes"/>
+ <point x="12.0" y="51.0"/>
+ <point x="56.0" y="-10.0"/>
+ </contour>
+ <contour>
+ <point x="128.0" y="40.0" type="curve" smooth="yes"/>
+ <point x="88.0" y="40.0"/>
+ <point x="69.0" y="71.0"/>
+ <point x="79.0" y="108.0" type="curve" smooth="yes"/>
+ <point x="111.0" y="229.0" type="line" smooth="yes"/>
+ <point x="123.0" y="273.0"/>
+ <point x="170.0" y="310.0"/>
+ <point x="214.0" y="310.0" type="curve" smooth="yes"/>
+ <point x="254.0" y="310.0"/>
+ <point x="273.0" y="279.0"/>
+ <point x="263.0" y="242.0" type="curve" smooth="yes"/>
+ <point x="231.0" y="121.0" type="line" smooth="yes"/>
+ <point x="219.0" y="77.0"/>
+ <point x="172.0" y="40.0"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif
new file mode 100644
index 00000000..d289e0d1
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/N_.glif
@@ -0,0 +1,47 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="N" format="2">
+ <advance width="408.0"/>
+ <unicode hex="004E"/>
+ <anchor x="157.0" y="0.0" name="bottom"/>
+ <anchor x="291.0" y="500.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="38.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="50.0" y="-4.0"/>
+ <point x="59.0" y="4.0"/>
+ <point x="62.0" y="15.0" type="curve" smooth="yes"/>
+ <point x="161.0" y="384.0" type="line"/>
+ <point x="264.0" y="14.0" type="line" smooth="yes"/>
+ <point x="267.0" y="2.0"/>
+ <point x="278.0" y="-4.0"/>
+ <point x="288.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="299.0" y="-4.0"/>
+ <point x="309.0" y="2.0"/>
+ <point x="312.0" y="14.0" type="curve" smooth="yes"/>
+ <point x="434.0" y="472.0" type="line" smooth="yes"/>
+ <point x="437.0" y="483.0"/>
+ <point x="430.0" y="504.0"/>
+ <point x="410.0" y="504.0" type="curve" smooth="yes"/>
+ <point x="398.0" y="504.0"/>
+ <point x="389.0" y="496.0"/>
+ <point x="386.0" y="485.0" type="curve" smooth="yes"/>
+ <point x="287.0" y="116.0" type="line"/>
+ <point x="184.0" y="486.0" type="line" smooth="yes"/>
+ <point x="181.0" y="498.0"/>
+ <point x="170.0" y="504.0"/>
+ <point x="159.0" y="504.0" type="curve" smooth="yes"/>
+ <point x="149.0" y="504.0"/>
+ <point x="139.0" y="498.0"/>
+ <point x="136.0" y="486.0" type="curve" smooth="yes"/>
+ <point x="14.0" y="28.0" type="line" smooth="yes"/>
+ <point x="10.0" y="12.0"/>
+ <point x="22.0" y="-4.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2018/11/20 10:52:27</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif
new file mode 100644
index 00000000..aafee0a9
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_.glif
@@ -0,0 +1,51 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="O" format="2">
+ <advance width="404.0"/>
+ <unicode hex="004F"/>
+ <anchor x="155.0" y="0.0" name="bottom"/>
+ <anchor x="222.0" y="250.0" name="center"/>
+ <anchor x="318.0" y="10.0" name="ogonek"/>
+ <anchor x="289.0" y="500.0" name="top"/>
+ <anchor x="109.0" y="500.0" name="topleft"/>
+ <anchor x="469.0" y="500.0" name="topright"/>
+ <outline>
+ <contour>
+ <point x="159.0" y="-10.0" type="curve" smooth="yes"/>
+ <point x="239.0" y="-10.0"/>
+ <point x="321.0" y="54.0"/>
+ <point x="342.0" y="133.0" type="curve" smooth="yes"/>
+ <point x="401.0" y="354.0" type="line" smooth="yes"/>
+ <point x="423.0" y="437.0"/>
+ <point x="371.0" y="510.0"/>
+ <point x="285.0" y="510.0" type="curve" smooth="yes"/>
+ <point x="205.0" y="510.0"/>
+ <point x="124.0" y="446.0"/>
+ <point x="103.0" y="366.0" type="curve" smooth="yes"/>
+ <point x="44.0" y="146.0" type="line" smooth="yes"/>
+ <point x="22.0" y="63.0"/>
+ <point x="73.0" y="-10.0"/>
+ </contour>
+ <contour>
+ <point x="159.0" y="40.0" type="curve" smooth="yes"/>
+ <point x="106.0" y="40.0"/>
+ <point x="78.0" y="81.0"/>
+ <point x="92.0" y="133.0" type="curve" smooth="yes"/>
+ <point x="151.0" y="354.0" type="line" smooth="yes"/>
+ <point x="166.0" y="412.0"/>
+ <point x="227.0" y="460.0"/>
+ <point x="285.0" y="460.0" type="curve" smooth="yes"/>
+ <point x="338.0" y="460.0"/>
+ <point x="367.0" y="419.0"/>
+ <point x="353.0" y="366.0" type="curve" smooth="yes"/>
+ <point x="294.0" y="146.0" type="line" smooth="yes"/>
+ <point x="278.0" y="88.0"/>
+ <point x="217.0" y="40.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2017/10/27 21:28:25</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif
new file mode 100644
index 00000000..93bca95f
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/O_dieresis.glif
@@ -0,0 +1,23 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="Odieresis" format="2">
+ <advance width="404.0"/>
+ <unicode hex="00D6"/>
+ <anchor x="155.0" y="0.0" name="bottom"/>
+ <anchor x="222.0" y="250.0" name="center"/>
+ <anchor x="318.0" y="10.0" name="ogonek"/>
+ <anchor x="322.0" y="625.0" name="top"/>
+ <anchor x="109.0" y="500.0" name="topleft"/>
+ <anchor x="469.0" y="500.0" name="topright"/>
+ <outline>
+ <component base="O"/>
+ <component base="dieresiscomb" xOffset="92.0" yOffset="150.0"/>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2017/10/27 21:28:35</string>
+ <key>public.markColor</key>
+ <string>0,0.67,0.91,1</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist
new file mode 100644
index 00000000..6a3662fd
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/contents.plist
@@ -0,0 +1,20 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>N</key>
+ <string>N_.glif</string>
+ <key>O</key>
+ <string>O_.glif</string>
+ <key>Odieresis</key>
+ <string>O_dieresis.glif</string>
+ <key>dieresiscomb</key>
+ <string>dieresiscomb.glif</string>
+ <key>n</key>
+ <string>n.glif</string>
+ <key>o</key>
+ <string>o.glif</string>
+ <key>odieresis</key>
+ <string>odieresis.glif</string>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif
new file mode 100644
index 00000000..224df4d5
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/dieresiscomb.glif
@@ -0,0 +1,48 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="dieresiscomb" format="2">
+ <unicode hex="0308"/>
+ <anchor x="197.0" y="350.0" name="_top"/>
+ <anchor x="230.0" y="475.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="300.0" y="425.0" type="curve" smooth="yes"/>
+ <point x="312.0" y="425.0"/>
+ <point x="321.0" y="433.0"/>
+ <point x="324.0" y="445.0" type="curve" smooth="yes"/>
+ <point x="330.0" y="469.0" type="line" smooth="yes"/>
+ <point x="334.0" y="484.0"/>
+ <point x="322.0" y="500.0"/>
+ <point x="306.0" y="500.0" type="curve" smooth="yes"/>
+ <point x="294.0" y="500.0"/>
+ <point x="285.0" y="492.0"/>
+ <point x="282.0" y="480.0" type="curve" smooth="yes"/>
+ <point x="276.0" y="456.0" type="line" smooth="yes"/>
+ <point x="272.0" y="441.0"/>
+ <point x="284.0" y="425.0"/>
+ </contour>
+ <contour>
+ <point x="150.0" y="425.0" type="curve" smooth="yes"/>
+ <point x="162.0" y="425.0"/>
+ <point x="171.0" y="433.0"/>
+ <point x="174.0" y="445.0" type="curve" smooth="yes"/>
+ <point x="180.0" y="469.0" type="line" smooth="yes"/>
+ <point x="184.0" y="484.0"/>
+ <point x="172.0" y="500.0"/>
+ <point x="156.0" y="500.0" type="curve" smooth="yes"/>
+ <point x="144.0" y="500.0"/>
+ <point x="135.0" y="492.0"/>
+ <point x="132.0" y="480.0" type="curve" smooth="yes"/>
+ <point x="126.0" y="456.0" type="line" smooth="yes"/>
+ <point x="122.0" y="441.0"/>
+ <point x="134.0" y="425.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2018/11/20 11:01:28</string>
+ <key>com.schriftgestaltung.Glyphs.originalWidth</key>
+ <real>300.0</real>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif
new file mode 100644
index 00000000..1313dd47
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/n.glif
@@ -0,0 +1,44 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="n" format="2">
+ <advance width="348.0"/>
+ <unicode hex="006E"/>
+ <anchor x="128.0" y="0.0" name="bottom"/>
+ <anchor x="222.0" y="350.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="34.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="46.0" y="-4.0"/>
+ <point x="55.0" y="4.0"/>
+ <point x="58.0" y="15.0" type="curve" smooth="yes"/>
+ <point x="134.0" y="300.0" type="line"/>
+ <point x="238.0" y="300.0" type="line" smooth="yes"/>
+ <point x="266.0" y="300.0"/>
+ <point x="278.0" y="282.0"/>
+ <point x="271.0" y="255.0" type="curve" smooth="yes"/>
+ <point x="210.0" y="28.0" type="line" smooth="yes"/>
+ <point x="205.0" y="11.0"/>
+ <point x="218.0" y="-4.0"/>
+ <point x="234.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="246.0" y="-4.0"/>
+ <point x="255.0" y="3.0"/>
+ <point x="258.0" y="15.0" type="curve" smooth="yes"/>
+ <point x="319.0" y="242.0" type="line" smooth="yes"/>
+ <point x="335.0" y="301.0"/>
+ <point x="299.0" y="350.0"/>
+ <point x="238.0" y="350.0" type="curve" smooth="yes"/>
+ <point x="118.0" y="350.0" type="line" smooth="yes"/>
+ <point x="102.993" y="350.0"/>
+ <point x="94.0" y="343.0"/>
+ <point x="90.0" y="329.0" type="curve" smooth="yes"/>
+ <point x="10.0" y="28.0" type="line" smooth="yes"/>
+ <point x="5.0" y="11.0"/>
+ <point x="18.0" y="-4.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2017/10/27 21:28:25</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif
new file mode 100644
index 00000000..ac3ff7e1
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/o.glif
@@ -0,0 +1,50 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="o" format="2">
+ <advance width="342.0"/>
+ <unicode hex="006F"/>
+ <anchor x="124.0" y="0.0" name="bottom"/>
+ <anchor x="171.0" y="175.0" name="center"/>
+ <anchor x="267.0" y="10.0" name="ogonek"/>
+ <anchor x="218.0" y="350.0" name="top"/>
+ <anchor x="373.0" y="350.0" name="topright"/>
+ <outline>
+ <contour>
+ <point x="129.0" y="-10.0" type="curve" smooth="yes"/>
+ <point x="199.0" y="-10.0"/>
+ <point x="260.0" y="39.0"/>
+ <point x="280.0" y="110.0" type="curve" smooth="yes"/>
+ <point x="312.0" y="226.0" type="line" smooth="yes"/>
+ <point x="333.0" y="303.0"/>
+ <point x="291.0" y="360.0"/>
+ <point x="215.0" y="360.0" type="curve" smooth="yes"/>
+ <point x="145.0" y="360.0"/>
+ <point x="84.0" y="311.0"/>
+ <point x="64.0" y="240.0" type="curve" smooth="yes"/>
+ <point x="32.0" y="124.0" type="line" smooth="yes"/>
+ <point x="11.0" y="47.0"/>
+ <point x="53.0" y="-10.0"/>
+ </contour>
+ <contour>
+ <point x="129.0" y="40.0" type="curve" smooth="yes"/>
+ <point x="86.0" y="40.0"/>
+ <point x="68.0" y="66.0"/>
+ <point x="80.0" y="110.0" type="curve" smooth="yes"/>
+ <point x="112.0" y="226.0" type="line" smooth="yes"/>
+ <point x="126.0" y="277.0"/>
+ <point x="167.0" y="310.0"/>
+ <point x="215.0" y="310.0" type="curve" smooth="yes"/>
+ <point x="258.0" y="310.0"/>
+ <point x="276.0" y="284.0"/>
+ <point x="264.0" y="240.0" type="curve" smooth="yes"/>
+ <point x="232.0" y="124.0" type="line" smooth="yes"/>
+ <point x="218.0" y="73.0"/>
+ <point x="177.0" y="40.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2018/11/20 11:01:11</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif
new file mode 100644
index 00000000..42c39d1a
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/glyphs/odieresis.glif
@@ -0,0 +1,22 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="odieresis" format="2">
+ <advance width="342.0"/>
+ <unicode hex="00F6"/>
+ <anchor x="124.0" y="0.0" name="bottom"/>
+ <anchor x="171.0" y="175.0" name="center"/>
+ <anchor x="267.0" y="10.0" name="ogonek"/>
+ <anchor x="251.0" y="475.0" name="top"/>
+ <anchor x="373.0" y="350.0" name="topright"/>
+ <outline>
+ <component base="o"/>
+ <component base="dieresiscomb" xOffset="21.0"/>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2018/11/20 10:59:58</string>
+ <key>public.markColor</key>
+ <string>0,0.67,0.91,1</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist
new file mode 100644
index 00000000..f9e8ed31
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/groups.plist
@@ -0,0 +1,34 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>public.kern1.O</key>
+ <array>
+ <string>O</string>
+ <string>Odieresis</string>
+ </array>
+ <key>public.kern1.n</key>
+ <array>
+ <string>n</string>
+ </array>
+ <key>public.kern1.o</key>
+ <array>
+ <string>o</string>
+ <string>odieresis</string>
+ </array>
+ <key>public.kern2.O</key>
+ <array>
+ <string>O</string>
+ <string>Odieresis</string>
+ </array>
+ <key>public.kern2.n</key>
+ <array>
+ <string>n</string>
+ </array>
+ <key>public.kern2.o</key>
+ <array>
+ <string>o</string>
+ <string>odieresis</string>
+ </array>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist
new file mode 100644
index 00000000..7120d0ba
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/layercontents.plist
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <array>
+ <array>
+ <string>public.default</string>
+ <string>glyphs</string>
+ </array>
+ <array>
+ <string>public.background</string>
+ <string>glyphs.public.background</string>
+ </array>
+ </array>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist
new file mode 100644
index 00000000..b9f2b929
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/lib.plist
@@ -0,0 +1,86 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>com.schriftgestaltung.appVersion</key>
+ <string>1179</string>
+ <key>com.schriftgestaltung.customName</key>
+ <string>Italic 15</string>
+ <key>com.schriftgestaltung.customParameter.GSFont.Axes</key>
+ <array>
+ <dict>
+ <key>Name</key>
+ <string>Slant</string>
+ <key>Tag</key>
+ <string>slnt</string>
+ </dict>
+ </array>
+ <key>com.schriftgestaltung.customParameter.GSFont.DisplayStrings</key>
+ <array>
+ <string>o/dieresiscomb ö</string>
+ </array>
+ <key>com.schriftgestaltung.customParameter.GSFont.Variation Font Origin</key>
+ <string>EB3D7718-A203-47FB-ABD4-8B7A501887ED</string>
+ <key>com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment</key>
+ <false/>
+ <key>com.schriftgestaltung.customParameter.GSFont.useNiceNames</key>
+ <integer>1</integer>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.Link Metrics With Master</key>
+ <string>Regular</string>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.Master Name</key>
+ <string>Italic 15</string>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue</key>
+ <real>-15.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue1</key>
+ <real>16.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue2</key>
+ <real>0.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue3</key>
+ <real>0.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.iconName</key>
+ <string></string>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.weightValue</key>
+ <real>-15.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.widthValue</key>
+ <real>100.0</real>
+ <key>com.schriftgestaltung.customValue</key>
+ <real>-15.0</real>
+ <key>com.schriftgestaltung.customValue1</key>
+ <real>16.0</real>
+ <key>com.schriftgestaltung.fontMasterOrder</key>
+ <integer>1</integer>
+ <key>com.schriftgestaltung.glyphOrder</key>
+ <false/>
+ <key>com.schriftgestaltung.keyboardIncrement</key>
+ <integer>1</integer>
+ <key>com.schriftgestaltung.weight</key>
+ <string>Regular</string>
+ <key>com.schriftgestaltung.weightValue</key>
+ <real>-15.0</real>
+ <key>com.schriftgestaltung.width</key>
+ <string>Regular</string>
+ <key>com.schriftgestaltung.widthValue</key>
+ <real>100.0</real>
+ <key>noodleExtremesAndInflections</key>
+ <integer>0</integer>
+ <key>noodleRemoveOverlap</key>
+ <integer>1</integer>
+ <key>noodleThickness</key>
+ <string>50.0</string>
+ <key>public.glyphOrder</key>
+ <array>
+ <string>N</string>
+ <string>O</string>
+ <string>Odieresis</string>
+ <string>n</string>
+ <string>o</string>
+ <string>odieresis</string>
+ <string>dieresiscomb</string>
+ </array>
+ <key>public.postscriptNames</key>
+ <dict>
+ <key>dieresiscomb</key>
+ <string>uni0308</string>
+ </dict>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist
new file mode 100644
index 00000000..7b8b34ac
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Italic15.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>creator</key>
+ <string>com.github.fonttools.ufoLib</string>
+ <key>formatVersion</key>
+ <integer>3</integer>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea
new file mode 100644
index 00000000..88e4c41a
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/features.fea
@@ -0,0 +1,23 @@
+# automatic
+@Uppercase = [ N O Odieresis ];
+
+# Prefix: Languagesystems
+# automatic
+languagesystem DFLT dflt;
+languagesystem latn dflt;
+languagesystem latn NLD;
+
+
+feature cpsp {
+pos @Uppercase <25 0 50 0>;
+
+} cpsp;
+
+table GDEF {
+ # automatic
+ GlyphClassDef
+ [N O Odieresis n o odieresis], # Base
+ , # Liga
+ [dieresiscomb], # Mark
+ ;
+} GDEF;
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist
new file mode 100644
index 00000000..7ff1edb1
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/fontinfo.plist
@@ -0,0 +1,93 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>ascender</key>
+ <real>600.0</real>
+ <key>capHeight</key>
+ <real>500.0</real>
+ <key>copyright</key>
+ <string>Copyright 2017 by Jens Kutilek</string>
+ <key>descender</key>
+ <real>-150.0</real>
+ <key>familyName</key>
+ <string>Test Family 4</string>
+ <key>guidelines</key>
+ <array>
+ <dict>
+ <key>angle</key>
+ <real>180.0</real>
+ <key>x</key>
+ <real>125.0</real>
+ <key>y</key>
+ <real>175.0</real>
+ </dict>
+ </array>
+ <key>italicAngle</key>
+ <real>-0.0</real>
+ <key>openTypeHeadCreated</key>
+ <string>2017/10/21 13:31:38</string>
+ <key>openTypeNameDesigner</key>
+ <string>Jens Kutilek after the ISO 3098 standard</string>
+ <key>openTypeNameDesignerURL</key>
+ <string>https://www.kutilek.de/</string>
+ <key>openTypeNameManufacturer</key>
+ <string>Jens Kutilek</string>
+ <key>openTypeNameManufacturerURL</key>
+ <string>https://www.kutilek.de/</string>
+ <key>openTypeOS2Type</key>
+ <array>
+ <integer>3</integer>
+ </array>
+ <key>openTypeOS2VendorID</key>
+ <string>jens</string>
+ <key>openTypeOS2WinAscent</key>
+ <integer>700</integer>
+ <key>openTypeOS2WinDescent</key>
+ <integer>250</integer>
+ <key>postscriptBlueValues</key>
+ <array>
+ <real>-10.0</real>
+ <real>0.0</real>
+ <real>350.0</real>
+ <real>360.0</real>
+ <real>500.0</real>
+ <real>510.0</real>
+ </array>
+ <key>postscriptFamilyBlues</key>
+ <array/>
+ <key>postscriptFamilyOtherBlues</key>
+ <array/>
+ <key>postscriptOtherBlues</key>
+ <array>
+ <real>-160.0</real>
+ <real>-150.0</real>
+ </array>
+ <key>postscriptStemSnapH</key>
+ <array>
+ <integer>50</integer>
+ </array>
+ <key>postscriptStemSnapV</key>
+ <array>
+ <integer>50</integer>
+ </array>
+ <key>postscriptUnderlinePosition</key>
+ <integer>-100</integer>
+ <key>postscriptUnderlineThickness</key>
+ <integer>50</integer>
+ <key>styleMapFamilyName</key>
+ <string>Test Family 4</string>
+ <key>styleMapStyleName</key>
+ <string>regular</string>
+ <key>styleName</key>
+ <string>Regular</string>
+ <key>unitsPerEm</key>
+ <integer>750</integer>
+ <key>versionMajor</key>
+ <integer>1</integer>
+ <key>versionMinor</key>
+ <integer>0</integer>
+ <key>xHeight</key>
+ <real>350.0</real>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif
new file mode 100644
index 00000000..941a558e
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/N_.glif
@@ -0,0 +1,31 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="N" format="2">
+ <outline>
+ <contour>
+ <point x="324.0" y="-22.0" type="line" smooth="yes"/>
+ <point x="336.0" y="-44.0"/>
+ <point x="354.0" y="-40.0"/>
+ <point x="354.0" y="-14.0" type="curve" smooth="yes"/>
+ <point x="354.0" y="479.0" type="line" smooth="yes"/>
+ <point x="354.0" y="493.0"/>
+ <point x="343.0" y="504.0"/>
+ <point x="329.0" y="504.0" type="curve" smooth="yes"/>
+ <point x="315.0" y="504.0"/>
+ <point x="304.0" y="493.0"/>
+ <point x="304.0" y="479.0" type="curve" smooth="yes"/>
+ <point x="304.0" y="119.0" type="line"/>
+ <point x="84.0" y="522.0" type="line" smooth="yes"/>
+ <point x="72.0" y="544.0"/>
+ <point x="54.0" y="540.0"/>
+ <point x="54.0" y="514.0" type="curve" smooth="yes"/>
+ <point x="54.0" y="21.0" type="line" smooth="yes"/>
+ <point x="54.0" y="7.0"/>
+ <point x="65.0" y="-4.0"/>
+ <point x="79.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="93.0" y="-4.0"/>
+ <point x="104.0" y="7.0"/>
+ <point x="104.0" y="21.0" type="curve" smooth="yes"/>
+ <point x="104.0" y="381.0" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif
new file mode 100644
index 00000000..dc9a72ed
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/O_.glif
@@ -0,0 +1,28 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="O" format="2">
+ <anchor x="202.0" y="0.0" name="bottom"/>
+ <anchor x="202.0" y="250.0" name="center"/>
+ <anchor x="362.0" y="10.0" name="ogonek"/>
+ <anchor x="202.0" y="500.0" name="top"/>
+ <anchor x="22.0" y="500.0" name="topleft"/>
+ <anchor x="382.0" y="500.0" name="topright"/>
+ <outline>
+ <contour>
+ <point x="77.0" y="150.0" type="curve"/>
+ <point x="77.0" y="350.0" type="line"/>
+ <point x="77.0" y="419.0"/>
+ <point x="133.0" y="475.0"/>
+ <point x="202.0" y="475.0" type="curve"/>
+ <point x="271.0" y="475.0"/>
+ <point x="327.0" y="419.0"/>
+ <point x="327.0" y="350.0" type="curve"/>
+ <point x="327.0" y="250.0" type="line"/>
+ <point x="327.0" y="150.0" type="line"/>
+ <point x="327.0" y="81.0"/>
+ <point x="271.0" y="25.0"/>
+ <point x="202.0" y="25.0" type="curve"/>
+ <point x="133.0" y="25.0"/>
+ <point x="77.0" y="81.0"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist
new file mode 100644
index 00000000..8382c790
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/contents.plist
@@ -0,0 +1,16 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>N</key>
+ <string>N_.glif</string>
+ <key>O</key>
+ <string>O_.glif</string>
+ <key>dieresiscomb</key>
+ <string>dieresiscomb.glif</string>
+ <key>n</key>
+ <string>n.glif</string>
+ <key>o</key>
+ <string>o.glif</string>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif
new file mode 100644
index 00000000..11fb86ed
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/dieresiscomb.glif
@@ -0,0 +1,15 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="dieresiscomb" format="2">
+ <anchor x="150.0" y="350.0" name="_top"/>
+ <anchor x="150.0" y="475.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="75.0" y="475.0" type="move"/>
+ <point x="75.0" y="450.0" type="line"/>
+ </contour>
+ <contour>
+ <point x="225.0" y="475.0" type="move"/>
+ <point x="225.0" y="450.0" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif
new file mode 100644
index 00000000..2d1a13e8
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/n.glif
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="n" format="2">
+ <outline>
+ <contour>
+ <point x="75.0" y="21.0" type="move"/>
+ <point x="75.0" y="325.0" type="line"/>
+ <point x="198.0" y="325.0" type="line" smooth="yes"/>
+ <point x="243.0" y="325.0"/>
+ <point x="275.0" y="293.0"/>
+ <point x="275.0" y="248.0" type="curve" smooth="yes"/>
+ <point x="275.0" y="21.0" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif
new file mode 100644
index 00000000..2e9a61bc
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs.public.background/o.glif
@@ -0,0 +1,37 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="o" format="2">
+ <outline>
+ <contour>
+ <point x="171.0" y="-10.0" type="curve" smooth="yes"/>
+ <point x="240.0" y="-10.0"/>
+ <point x="296.0" y="46.0"/>
+ <point x="296.0" y="115.0" type="curve" smooth="yes"/>
+ <point x="296.0" y="235.0" type="line" smooth="yes"/>
+ <point x="296.0" y="304.0"/>
+ <point x="240.0" y="360.0"/>
+ <point x="171.0" y="360.0" type="curve" smooth="yes"/>
+ <point x="102.0" y="360.0"/>
+ <point x="46.0" y="304.0"/>
+ <point x="46.0" y="235.0" type="curve" smooth="yes"/>
+ <point x="46.0" y="115.0" type="line" smooth="yes"/>
+ <point x="46.0" y="46.0"/>
+ <point x="102.0" y="-10.0"/>
+ </contour>
+ <contour>
+ <point x="171.0" y="40.0" type="curve" smooth="yes"/>
+ <point x="130.0" y="40.0"/>
+ <point x="96.0" y="74.0"/>
+ <point x="96.0" y="115.0" type="curve" smooth="yes"/>
+ <point x="96.0" y="235.0" type="line" smooth="yes"/>
+ <point x="96.0" y="276.0"/>
+ <point x="130.0" y="310.0"/>
+ <point x="171.0" y="310.0" type="curve" smooth="yes"/>
+ <point x="212.0" y="310.0"/>
+ <point x="246.0" y="276.0"/>
+ <point x="246.0" y="235.0" type="curve" smooth="yes"/>
+ <point x="246.0" y="115.0" type="line" smooth="yes"/>
+ <point x="246.0" y="74.0"/>
+ <point x="212.0" y="40.0"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif
new file mode 100644
index 00000000..a588dac2
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/N_.glif
@@ -0,0 +1,47 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="N" format="2">
+ <advance width="408.0"/>
+ <unicode hex="004E"/>
+ <anchor x="204.0" y="0.0" name="bottom"/>
+ <anchor x="204.0" y="500.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="79.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="93.0" y="-4.0"/>
+ <point x="104.0" y="7.0"/>
+ <point x="104.0" y="21.0" type="curve" smooth="yes"/>
+ <point x="104.0" y="381.0" type="line"/>
+ <point x="307.0" y="9.0" type="line" smooth="yes"/>
+ <point x="311.0" y="2.0"/>
+ <point x="317.0" y="-4.0"/>
+ <point x="329.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="343.0" y="-4.0"/>
+ <point x="354.0" y="7.0"/>
+ <point x="354.0" y="21.0" type="curve" smooth="yes"/>
+ <point x="354.0" y="479.0" type="line" smooth="yes"/>
+ <point x="354.0" y="493.0"/>
+ <point x="343.0" y="504.0"/>
+ <point x="329.0" y="504.0" type="curve" smooth="yes"/>
+ <point x="315.0" y="504.0"/>
+ <point x="304.0" y="493.0"/>
+ <point x="304.0" y="479.0" type="curve" smooth="yes"/>
+ <point x="304.0" y="119.0" type="line"/>
+ <point x="101.0" y="491.0" type="line" smooth="yes"/>
+ <point x="97.0" y="498.0"/>
+ <point x="91.0" y="504.0"/>
+ <point x="79.0" y="504.0" type="curve" smooth="yes"/>
+ <point x="65.0" y="504.0"/>
+ <point x="54.0" y="493.0"/>
+ <point x="54.0" y="479.0" type="curve" smooth="yes"/>
+ <point x="54.0" y="21.0" type="line" smooth="yes"/>
+ <point x="54.0" y="7.0"/>
+ <point x="65.0" y="-4.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2018/11/20 10:52:27</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif
new file mode 100644
index 00000000..f3979ed5
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_.glif
@@ -0,0 +1,51 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="O" format="2">
+ <advance width="404.0"/>
+ <unicode hex="004F"/>
+ <anchor x="202.0" y="0.0" name="bottom"/>
+ <anchor x="202.0" y="250.0" name="center"/>
+ <anchor x="362.0" y="10.0" name="ogonek"/>
+ <anchor x="202.0" y="500.0" name="top"/>
+ <anchor x="22.0" y="500.0" name="topleft"/>
+ <anchor x="382.0" y="500.0" name="topright"/>
+ <outline>
+ <contour>
+ <point x="202.0" y="-10.0" type="curve" smooth="yes"/>
+ <point x="284.0" y="-10.0"/>
+ <point x="352.0" y="58.0"/>
+ <point x="352.0" y="140.0" type="curve" smooth="yes"/>
+ <point x="352.0" y="360.0" type="line" smooth="yes"/>
+ <point x="352.0" y="442.0"/>
+ <point x="284.0" y="510.0"/>
+ <point x="202.0" y="510.0" type="curve" smooth="yes"/>
+ <point x="120.0" y="510.0"/>
+ <point x="52.0" y="442.0"/>
+ <point x="52.0" y="360.0" type="curve" smooth="yes"/>
+ <point x="52.0" y="140.0" type="line" smooth="yes"/>
+ <point x="52.0" y="58.0"/>
+ <point x="120.0" y="-10.0"/>
+ </contour>
+ <contour>
+ <point x="202.0" y="40.0" type="curve" smooth="yes"/>
+ <point x="147.0" y="40.0"/>
+ <point x="102.0" y="85.0"/>
+ <point x="102.0" y="140.0" type="curve" smooth="yes"/>
+ <point x="102.0" y="360.0" type="line" smooth="yes"/>
+ <point x="102.0" y="415.0"/>
+ <point x="147.0" y="460.0"/>
+ <point x="202.0" y="460.0" type="curve" smooth="yes"/>
+ <point x="257.0" y="460.0"/>
+ <point x="302.0" y="415.0"/>
+ <point x="302.0" y="360.0" type="curve" smooth="yes"/>
+ <point x="302.0" y="140.0" type="line" smooth="yes"/>
+ <point x="302.0" y="85.0"/>
+ <point x="257.0" y="40.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2017/10/27 21:28:25</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif
new file mode 100644
index 00000000..22fcd379
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/O_dieresis.glif
@@ -0,0 +1,23 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="Odieresis" format="2">
+ <advance width="404.0"/>
+ <unicode hex="00D6"/>
+ <anchor x="202.0" y="0.0" name="bottom"/>
+ <anchor x="202.0" y="250.0" name="center"/>
+ <anchor x="362.0" y="10.0" name="ogonek"/>
+ <anchor x="202.0" y="625.0" name="top"/>
+ <anchor x="22.0" y="500.0" name="topleft"/>
+ <anchor x="382.0" y="500.0" name="topright"/>
+ <outline>
+ <component base="O"/>
+ <component base="dieresiscomb" xOffset="52.0" yOffset="150.0"/>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2017/10/27 21:28:35</string>
+ <key>public.markColor</key>
+ <string>0,0.67,0.91,1</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist
new file mode 100644
index 00000000..6a3662fd
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/contents.plist
@@ -0,0 +1,20 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>N</key>
+ <string>N_.glif</string>
+ <key>O</key>
+ <string>O_.glif</string>
+ <key>Odieresis</key>
+ <string>O_dieresis.glif</string>
+ <key>dieresiscomb</key>
+ <string>dieresiscomb.glif</string>
+ <key>n</key>
+ <string>n.glif</string>
+ <key>o</key>
+ <string>o.glif</string>
+ <key>odieresis</key>
+ <string>odieresis.glif</string>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif
new file mode 100644
index 00000000..b3cfef51
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/dieresiscomb.glif
@@ -0,0 +1,48 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="dieresiscomb" format="2">
+ <unicode hex="0308"/>
+ <anchor x="150.0" y="350.0" name="_top"/>
+ <anchor x="150.0" y="475.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="225.0" y="425.0" type="curve" smooth="yes"/>
+ <point x="239.0" y="425.0"/>
+ <point x="250.0" y="436.0"/>
+ <point x="250.0" y="450.0" type="curve" smooth="yes"/>
+ <point x="250.0" y="475.0" type="line" smooth="yes"/>
+ <point x="250.0" y="489.0"/>
+ <point x="239.0" y="500.0"/>
+ <point x="225.0" y="500.0" type="curve" smooth="yes"/>
+ <point x="211.0" y="500.0"/>
+ <point x="200.0" y="489.0"/>
+ <point x="200.0" y="475.0" type="curve" smooth="yes"/>
+ <point x="200.0" y="450.0" type="line" smooth="yes"/>
+ <point x="200.0" y="436.0"/>
+ <point x="211.0" y="425.0"/>
+ </contour>
+ <contour>
+ <point x="75.0" y="425.0" type="curve" smooth="yes"/>
+ <point x="89.0" y="425.0"/>
+ <point x="100.0" y="436.0"/>
+ <point x="100.0" y="450.0" type="curve" smooth="yes"/>
+ <point x="100.0" y="475.0" type="line" smooth="yes"/>
+ <point x="100.0" y="489.0"/>
+ <point x="89.0" y="500.0"/>
+ <point x="75.0" y="500.0" type="curve" smooth="yes"/>
+ <point x="61.0" y="500.0"/>
+ <point x="50.0" y="489.0"/>
+ <point x="50.0" y="475.0" type="curve" smooth="yes"/>
+ <point x="50.0" y="450.0" type="line" smooth="yes"/>
+ <point x="50.0" y="436.0"/>
+ <point x="61.0" y="425.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2018/11/20 11:01:28</string>
+ <key>com.schriftgestaltung.Glyphs.originalWidth</key>
+ <real>300.0</real>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif
new file mode 100644
index 00000000..221ce82c
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/n.glif
@@ -0,0 +1,44 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="n" format="2">
+ <advance width="348.0"/>
+ <unicode hex="006E"/>
+ <anchor x="175.0" y="0.0" name="bottom"/>
+ <anchor x="175.0" y="350.0" name="top"/>
+ <outline>
+ <contour>
+ <point x="75.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="89.0" y="-4.0"/>
+ <point x="100.0" y="7.0"/>
+ <point x="100.0" y="21.0" type="curve" smooth="yes"/>
+ <point x="100.0" y="300.0" type="line"/>
+ <point x="198.0" y="300.0" type="line" smooth="yes"/>
+ <point x="229.0" y="300.0"/>
+ <point x="250.0" y="279.0"/>
+ <point x="250.0" y="248.0" type="curve" smooth="yes"/>
+ <point x="250.0" y="21.0" type="line" smooth="yes"/>
+ <point x="250.0" y="7.0"/>
+ <point x="261.0" y="-4.0"/>
+ <point x="275.0" y="-4.0" type="curve" smooth="yes"/>
+ <point x="289.0" y="-4.0"/>
+ <point x="300.0" y="7.0"/>
+ <point x="300.0" y="21.0" type="curve" smooth="yes"/>
+ <point x="300.0" y="248.0" type="line" smooth="yes"/>
+ <point x="300.0" y="307.0"/>
+ <point x="257.0" y="350.0"/>
+ <point x="198.0" y="350.0" type="curve" smooth="yes"/>
+ <point x="75.0" y="350.0" type="line" smooth="yes"/>
+ <point x="59.0" y="350.0"/>
+ <point x="50.0" y="341.0"/>
+ <point x="50.0" y="325.0" type="curve" smooth="yes"/>
+ <point x="50.0" y="21.0" type="line" smooth="yes"/>
+ <point x="50.0" y="7.0"/>
+ <point x="61.0" y="-4.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2017/10/27 21:28:25</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif
new file mode 100644
index 00000000..417e4ff8
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/o.glif
@@ -0,0 +1,50 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="o" format="2">
+ <advance width="342.0"/>
+ <unicode hex="006F"/>
+ <anchor x="171.0" y="0.0" name="bottom"/>
+ <anchor x="171.0" y="175.0" name="center"/>
+ <anchor x="311.0" y="10.0" name="ogonek"/>
+ <anchor x="171.0" y="350.0" name="top"/>
+ <anchor x="326.0" y="350.0" name="topright"/>
+ <outline>
+ <contour>
+ <point x="171.0" y="-10.0" type="curve" smooth="yes"/>
+ <point x="244.0" y="-10.0"/>
+ <point x="296.0" y="43.0"/>
+ <point x="296.0" y="117.0" type="curve" smooth="yes"/>
+ <point x="296.0" y="233.0" type="line" smooth="yes"/>
+ <point x="296.0" y="307.0"/>
+ <point x="244.0" y="360.0"/>
+ <point x="171.0" y="360.0" type="curve" smooth="yes"/>
+ <point x="98.0" y="360.0"/>
+ <point x="46.0" y="307.0"/>
+ <point x="46.0" y="233.0" type="curve" smooth="yes"/>
+ <point x="46.0" y="117.0" type="line" smooth="yes"/>
+ <point x="46.0" y="43.0"/>
+ <point x="98.0" y="-10.0"/>
+ </contour>
+ <contour>
+ <point x="171.0" y="40.0" type="curve" smooth="yes"/>
+ <point x="126.0" y="40.0"/>
+ <point x="96.0" y="70.0"/>
+ <point x="96.0" y="117.0" type="curve" smooth="yes"/>
+ <point x="96.0" y="233.0" type="line" smooth="yes"/>
+ <point x="96.0" y="280.0"/>
+ <point x="126.0" y="310.0"/>
+ <point x="171.0" y="310.0" type="curve" smooth="yes"/>
+ <point x="216.0" y="310.0"/>
+ <point x="246.0" y="280.0"/>
+ <point x="246.0" y="233.0" type="curve" smooth="yes"/>
+ <point x="246.0" y="117.0" type="line" smooth="yes"/>
+ <point x="246.0" y="70.0"/>
+ <point x="216.0" y="40.0"/>
+ </contour>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2018/11/20 11:01:11</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif
new file mode 100644
index 00000000..b0f2a597
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/glyphs/odieresis.glif
@@ -0,0 +1,22 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="odieresis" format="2">
+ <advance width="342.0"/>
+ <unicode hex="00F6"/>
+ <anchor x="171.0" y="0.0" name="bottom"/>
+ <anchor x="171.0" y="175.0" name="center"/>
+ <anchor x="311.0" y="10.0" name="ogonek"/>
+ <anchor x="171.0" y="475.0" name="top"/>
+ <anchor x="326.0" y="350.0" name="topright"/>
+ <outline>
+ <component base="o"/>
+ <component base="dieresiscomb" xOffset="21.0"/>
+ </outline>
+ <lib>
+ <dict>
+ <key>com.schriftgestaltung.Glyphs.lastChange</key>
+ <string>2018/11/20 10:59:58</string>
+ <key>public.markColor</key>
+ <string>0,0.67,0.91,1</string>
+ </dict>
+ </lib>
+</glyph>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist
new file mode 100644
index 00000000..f9e8ed31
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/groups.plist
@@ -0,0 +1,34 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>public.kern1.O</key>
+ <array>
+ <string>O</string>
+ <string>Odieresis</string>
+ </array>
+ <key>public.kern1.n</key>
+ <array>
+ <string>n</string>
+ </array>
+ <key>public.kern1.o</key>
+ <array>
+ <string>o</string>
+ <string>odieresis</string>
+ </array>
+ <key>public.kern2.O</key>
+ <array>
+ <string>O</string>
+ <string>Odieresis</string>
+ </array>
+ <key>public.kern2.n</key>
+ <array>
+ <string>n</string>
+ </array>
+ <key>public.kern2.o</key>
+ <array>
+ <string>o</string>
+ <string>odieresis</string>
+ </array>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist
new file mode 100644
index 00000000..8da68f2e
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/kerning.plist
@@ -0,0 +1,468 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>public.kern1.A</key>
+ <dict>
+ <key>public.kern2.O</key>
+ <real>-5.0</real>
+ <key>public.kern2.T</key>
+ <real>-40.0</real>
+ <key>public.kern2.V</key>
+ <real>-40.0</real>
+ <key>public.kern2.W</key>
+ <real>-25.0</real>
+ <key>public.kern2.Y</key>
+ <real>-50.0</real>
+ <key>public.kern2.f</key>
+ <real>-15.0</real>
+ <key>public.kern2.hyphen</key>
+ <real>-10.0</real>
+ <key>public.kern2.quote</key>
+ <real>-80.0</real>
+ <key>public.kern2.w</key>
+ <real>-25.0</real>
+ <key>public.kern2.y</key>
+ <real>-30.0</real>
+ </dict>
+ <key>public.kern1.B</key>
+ <dict>
+ <key>public.kern2.quote</key>
+ <real>-10.0</real>
+ </dict>
+ <key>public.kern1.C</key>
+ <dict>
+ <key>public.kern2.O</key>
+ <real>-5.0</real>
+ </dict>
+ <key>public.kern1.E</key>
+ <dict>
+ <key>public.kern2.O</key>
+ <real>-5.0</real>
+ </dict>
+ <key>public.kern1.F</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-35.0</real>
+ <key>public.kern2.a</key>
+ <real>-25.0</real>
+ <key>public.kern2.o</key>
+ <real>-20.0</real>
+ <key>public.kern2.period</key>
+ <real>-40.0</real>
+ <key>public.kern2.u</key>
+ <real>-15.0</real>
+ </dict>
+ <key>public.kern1.K</key>
+ <dict>
+ <key>public.kern2.O</key>
+ <real>-10.0</real>
+ </dict>
+ <key>public.kern1.L</key>
+ <dict>
+ <key>public.kern2.O</key>
+ <real>-20.0</real>
+ <key>public.kern2.T</key>
+ <real>-60.0</real>
+ <key>public.kern2.V</key>
+ <real>-65.0</real>
+ <key>public.kern2.W</key>
+ <real>-50.0</real>
+ <key>public.kern2.Y</key>
+ <real>-70.0</real>
+ <key>public.kern2.hyphen</key>
+ <real>-30.0</real>
+ <key>public.kern2.quote</key>
+ <real>-130.0</real>
+ </dict>
+ <key>public.kern1.O</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-5.0</real>
+ <key>public.kern2.J</key>
+ <real>-15.0</real>
+ <key>public.kern2.T</key>
+ <real>-15.0</real>
+ <key>public.kern2.V</key>
+ <real>-5.0</real>
+ <key>public.kern2.W</key>
+ <real>-15.0</real>
+ <key>public.kern2.X</key>
+ <real>-15.0</real>
+ <key>public.kern2.Y</key>
+ <real>-20.0</real>
+ <key>public.kern2.quote</key>
+ <real>-5.0</real>
+ </dict>
+ <key>public.kern1.P</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-30.0</real>
+ <key>public.kern2.J</key>
+ <real>-80.0</real>
+ <key>public.kern2.period</key>
+ <real>-50.0</real>
+ <key>public.kern2.quote</key>
+ <real>15.0</real>
+ </dict>
+ <key>public.kern1.R</key>
+ <dict>
+ <key>public.kern2.T</key>
+ <real>-15.0</real>
+ <key>public.kern2.W</key>
+ <real>-5.0</real>
+ <key>public.kern2.Y</key>
+ <real>-10.0</real>
+ <key>public.kern2.o</key>
+ <real>-10.0</real>
+ <key>public.kern2.quote</key>
+ <real>10.0</real>
+ </dict>
+ <key>public.kern1.S</key>
+ <dict>
+ <key>public.kern2.quote</key>
+ <real>-10.0</real>
+ </dict>
+ <key>public.kern1.T</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-40.0</real>
+ <key>public.kern2.J</key>
+ <real>-50.0</real>
+ <key>public.kern2.O</key>
+ <real>-15.0</real>
+ <key>public.kern2.a</key>
+ <real>-50.0</real>
+ <key>public.kern2.hyphen</key>
+ <real>-50.0</real>
+ <key>public.kern2.n</key>
+ <real>-40.0</real>
+ <key>public.kern2.o</key>
+ <real>-50.0</real>
+ <key>public.kern2.period</key>
+ <real>-60.0</real>
+ <key>public.kern2.s</key>
+ <real>-50.0</real>
+ <key>public.kern2.u</key>
+ <real>-40.0</real>
+ <key>public.kern2.w</key>
+ <real>-40.0</real>
+ <key>public.kern2.y</key>
+ <real>-40.0</real>
+ </dict>
+ <key>public.kern1.V</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-40.0</real>
+ <key>public.kern2.O</key>
+ <real>-5.0</real>
+ <key>public.kern2.a</key>
+ <real>-45.0</real>
+ <key>public.kern2.f</key>
+ <real>-15.0</real>
+ <key>public.kern2.hyphen</key>
+ <real>-30.0</real>
+ <key>public.kern2.n</key>
+ <real>-30.0</real>
+ <key>public.kern2.o</key>
+ <real>-45.0</real>
+ <key>public.kern2.period</key>
+ <real>-70.0</real>
+ <key>public.kern2.quote</key>
+ <real>-10.0</real>
+ <key>public.kern2.s</key>
+ <real>-35.0</real>
+ <key>public.kern2.u</key>
+ <real>-35.0</real>
+ <key>public.kern2.w</key>
+ <real>-35.0</real>
+ <key>public.kern2.x</key>
+ <real>-40.0</real>
+ <key>public.kern2.y</key>
+ <real>-35.0</real>
+ <key>public.kern2.z</key>
+ <real>-40.0</real>
+ </dict>
+ <key>public.kern1.W</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-25.0</real>
+ <key>public.kern2.O</key>
+ <real>-15.0</real>
+ <key>public.kern2.a</key>
+ <real>-25.0</real>
+ <key>public.kern2.f</key>
+ <real>-15.0</real>
+ <key>public.kern2.hyphen</key>
+ <real>-20.0</real>
+ <key>public.kern2.n</key>
+ <real>-20.0</real>
+ <key>public.kern2.o</key>
+ <real>-30.0</real>
+ <key>public.kern2.period</key>
+ <real>-40.0</real>
+ <key>public.kern2.quote</key>
+ <real>-10.0</real>
+ <key>public.kern2.s</key>
+ <real>-20.0</real>
+ <key>public.kern2.u</key>
+ <real>-30.0</real>
+ <key>public.kern2.w</key>
+ <real>-25.0</real>
+ <key>public.kern2.x</key>
+ <real>-30.0</real>
+ <key>public.kern2.y</key>
+ <real>-35.0</real>
+ <key>public.kern2.z</key>
+ <real>-25.0</real>
+ </dict>
+ <key>public.kern1.X</key>
+ <dict>
+ <key>public.kern2.O</key>
+ <real>-15.0</real>
+ <key>public.kern2.quote</key>
+ <real>-10.0</real>
+ </dict>
+ <key>public.kern1.Y</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-50.0</real>
+ <key>public.kern2.O</key>
+ <real>-20.0</real>
+ <key>public.kern2.a</key>
+ <real>-60.0</real>
+ <key>public.kern2.f</key>
+ <real>-20.0</real>
+ <key>public.kern2.hyphen</key>
+ <real>-60.0</real>
+ <key>public.kern2.n</key>
+ <real>-40.0</real>
+ <key>public.kern2.o</key>
+ <real>-60.0</real>
+ <key>public.kern2.period</key>
+ <real>-60.0</real>
+ <key>public.kern2.quote</key>
+ <real>-5.0</real>
+ <key>public.kern2.s</key>
+ <real>-45.0</real>
+ <key>public.kern2.u</key>
+ <real>-40.0</real>
+ </dict>
+ <key>public.kern1.Z</key>
+ <dict>
+ <key>public.kern2.quote</key>
+ <real>-5.0</real>
+ </dict>
+ <key>public.kern1.c</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-10.0</real>
+ <key>public.kern2.quote</key>
+ <real>20.0</real>
+ </dict>
+ <key>public.kern1.f</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-15.0</real>
+ <key>public.kern2.o</key>
+ <real>-15.0</real>
+ <key>public.kern2.period</key>
+ <real>-40.0</real>
+ <key>public.kern2.quote</key>
+ <real>45.0</real>
+ </dict>
+ <key>public.kern1.hyphen</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-10.0</real>
+ <key>public.kern2.T</key>
+ <real>-50.0</real>
+ <key>public.kern2.V</key>
+ <real>-30.0</real>
+ <key>public.kern2.W</key>
+ <real>-20.0</real>
+ <key>public.kern2.Y</key>
+ <real>-60.0</real>
+ <key>public.kern2.f</key>
+ <real>-5.0</real>
+ <key>public.kern2.j</key>
+ <real>-5.0</real>
+ <key>public.kern2.period</key>
+ <real>-20.0</real>
+ <key>public.kern2.quote</key>
+ <real>-40.0</real>
+ <key>public.kern2.s</key>
+ <real>-5.0</real>
+ <key>public.kern2.w</key>
+ <real>-10.0</real>
+ <key>public.kern2.x</key>
+ <real>-25.0</real>
+ <key>public.kern2.y</key>
+ <real>-10.0</real>
+ <key>public.kern2.z</key>
+ <real>-20.0</real>
+ </dict>
+ <key>public.kern1.k</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-5.0</real>
+ </dict>
+ <key>public.kern1.l</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-10.0</real>
+ <key>public.kern2.period</key>
+ <real>15.0</real>
+ <key>public.kern2.quote</key>
+ <real>-15.0</real>
+ </dict>
+ <key>public.kern1.n</key>
+ <dict>
+ <key>public.kern2.quote</key>
+ <real>-10.0</real>
+ <key>public.kern2.y</key>
+ <real>-5.0</real>
+ </dict>
+ <key>public.kern1.o</key>
+ <dict>
+ <key>public.kern2.period</key>
+ <real>-10.0</real>
+ <key>public.kern2.quote</key>
+ <real>-10.0</real>
+ <key>public.kern2.w</key>
+ <real>-8.0</real>
+ <key>public.kern2.y</key>
+ <real>-8.0</real>
+ </dict>
+ <key>public.kern1.period</key>
+ <dict>
+ <key>public.kern2.T</key>
+ <real>-60.0</real>
+ <key>public.kern2.V</key>
+ <real>-70.0</real>
+ <key>public.kern2.W</key>
+ <real>-40.0</real>
+ <key>public.kern2.Y</key>
+ <real>-60.0</real>
+ <key>public.kern2.f</key>
+ <real>-20.0</real>
+ <key>public.kern2.hyphen</key>
+ <real>-20.0</real>
+ <key>public.kern2.l</key>
+ <real>-5.0</real>
+ <key>public.kern2.o</key>
+ <real>-10.0</real>
+ <key>public.kern2.quote</key>
+ <real>-60.0</real>
+ <key>public.kern2.u</key>
+ <real>-10.0</real>
+ <key>public.kern2.w</key>
+ <real>-30.0</real>
+ <key>public.kern2.x</key>
+ <real>10.0</real>
+ <key>public.kern2.y</key>
+ <real>-30.0</real>
+ <key>public.kern2.z</key>
+ <real>5.0</real>
+ </dict>
+ <key>public.kern1.quote</key>
+ <dict>
+ <key>public.kern2.A</key>
+ <real>-90.0</real>
+ <key>public.kern2.J</key>
+ <real>-110.0</real>
+ <key>public.kern2.O</key>
+ <real>-10.0</real>
+ <key>public.kern2.T</key>
+ <real>10.0</real>
+ <key>public.kern2.a</key>
+ <real>-30.0</real>
+ <key>public.kern2.f</key>
+ <real>5.0</real>
+ <key>public.kern2.hyphen</key>
+ <real>-90.0</real>
+ <key>public.kern2.n</key>
+ <real>-10.0</real>
+ <key>public.kern2.o</key>
+ <real>-25.0</real>
+ <key>public.kern2.period</key>
+ <real>-80.0</real>
+ <key>public.kern2.s</key>
+ <real>-15.0</real>
+ <key>public.kern2.u</key>
+ <real>-5.0</real>
+ <key>public.kern2.z</key>
+ <real>-5.0</real>
+ </dict>
+ <key>public.kern1.r</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-5.0</real>
+ <key>public.kern2.period</key>
+ <real>-35.0</real>
+ <key>public.kern2.quote</key>
+ <real>20.0</real>
+ </dict>
+ <key>public.kern1.s</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-10.0</real>
+ </dict>
+ <key>public.kern1.t</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-15.0</real>
+ <key>public.kern2.o</key>
+ <real>-15.0</real>
+ <key>public.kern2.period</key>
+ <real>-40.0</real>
+ <key>public.kern2.quote</key>
+ <real>20.0</real>
+ </dict>
+ <key>public.kern1.u</key>
+ <dict>
+ <key>public.kern2.quote</key>
+ <real>15.0</real>
+ </dict>
+ <key>public.kern1.w</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-10.0</real>
+ <key>public.kern2.o</key>
+ <real>-8.0</real>
+ <key>public.kern2.period</key>
+ <real>-30.0</real>
+ <key>public.kern2.quote</key>
+ <real>15.0</real>
+ </dict>
+ <key>public.kern1.x</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-25.0</real>
+ <key>public.kern2.period</key>
+ <real>10.0</real>
+ <key>public.kern2.quote</key>
+ <real>15.0</real>
+ </dict>
+ <key>public.kern1.y</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-10.0</real>
+ <key>public.kern2.o</key>
+ <real>-8.0</real>
+ <key>public.kern2.period</key>
+ <real>-30.0</real>
+ <key>public.kern2.quote</key>
+ <real>15.0</real>
+ </dict>
+ <key>public.kern1.z</key>
+ <dict>
+ <key>public.kern2.hyphen</key>
+ <real>-10.0</real>
+ <key>public.kern2.period</key>
+ <real>5.0</real>
+ <key>public.kern2.quote</key>
+ <real>20.0</real>
+ </dict>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist
new file mode 100644
index 00000000..7120d0ba
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/layercontents.plist
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <array>
+ <array>
+ <string>public.default</string>
+ <string>glyphs</string>
+ </array>
+ <array>
+ <string>public.background</string>
+ <string>glyphs.public.background</string>
+ </array>
+ </array>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist
new file mode 100644
index 00000000..7e5ced48
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/lib.plist
@@ -0,0 +1,80 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>com.schriftgestaltung.appVersion</key>
+ <string>1179</string>
+ <key>com.schriftgestaltung.customParameter.GSFont.Axes</key>
+ <array>
+ <dict>
+ <key>Name</key>
+ <string>Slant</string>
+ <key>Tag</key>
+ <string>slnt</string>
+ </dict>
+ </array>
+ <key>com.schriftgestaltung.customParameter.GSFont.DisplayStrings</key>
+ <array>
+ <string>o/dieresiscomb ö</string>
+ </array>
+ <key>com.schriftgestaltung.customParameter.GSFont.Variation Font Origin</key>
+ <string>EB3D7718-A203-47FB-ABD4-8B7A501887ED</string>
+ <key>com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment</key>
+ <false/>
+ <key>com.schriftgestaltung.customParameter.GSFont.useNiceNames</key>
+ <integer>1</integer>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.Master Name</key>
+ <string>Regular</string>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue</key>
+ <real>0.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue1</key>
+ <real>16.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue2</key>
+ <real>0.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.customValue3</key>
+ <real>0.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.iconName</key>
+ <string></string>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.weightValue</key>
+ <real>0.0</real>
+ <key>com.schriftgestaltung.customParameter.GSFontMaster.widthValue</key>
+ <real>100.0</real>
+ <key>com.schriftgestaltung.customValue1</key>
+ <real>16.0</real>
+ <key>com.schriftgestaltung.fontMasterOrder</key>
+ <integer>0</integer>
+ <key>com.schriftgestaltung.glyphOrder</key>
+ <false/>
+ <key>com.schriftgestaltung.keyboardIncrement</key>
+ <integer>1</integer>
+ <key>com.schriftgestaltung.weight</key>
+ <string>Regular</string>
+ <key>com.schriftgestaltung.weightValue</key>
+ <real>0.0</real>
+ <key>com.schriftgestaltung.width</key>
+ <string>Regular</string>
+ <key>com.schriftgestaltung.widthValue</key>
+ <real>100.0</real>
+ <key>noodleExtremesAndInflections</key>
+ <integer>0</integer>
+ <key>noodleRemoveOverlap</key>
+ <integer>1</integer>
+ <key>noodleThickness</key>
+ <string>50.0</string>
+ <key>public.glyphOrder</key>
+ <array>
+ <string>N</string>
+ <string>O</string>
+ <string>Odieresis</string>
+ <string>n</string>
+ <string>o</string>
+ <string>odieresis</string>
+ <string>dieresiscomb</string>
+ </array>
+ <key>public.postscriptNames</key>
+ <dict>
+ <key>dieresiscomb</key>
+ <string>uni0308</string>
+ </dict>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist
new file mode 100644
index 00000000..7b8b34ac
--- /dev/null
+++ b/Tests/varLib/data/master_ufo/TestFamily4-Regular.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>creator</key>
+ <string>com.github.fonttools.ufoLib</string>
+ <key>formatVersion</key>
+ <integer>3</integer>
+ </dict>
+</plist>
diff --git a/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx b/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx
new file mode 100644
index 00000000..ce5b55d2
--- /dev/null
+++ b/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
+
+ <gvar>
+ <version value="1"/>
+ <reserved value="0"/>
+ <glyphVariations glyph="N">
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="-41" y="0"/>
+ <delta pt="1" x="-43" y="0"/>
+ <delta pt="2" x="-43" y="5"/>
+ <delta pt="3" x="-40" y="7"/>
+ <delta pt="4" x="82" y="7"/>
+ <delta pt="5" x="84" y="5"/>
+ <delta pt="6" x="83" y="0"/>
+ <delta pt="7" x="80" y="0"/>
+ <delta pt="8" x="79" y="0"/>
+ <delta pt="9" x="84" y="-1"/>
+ <delta pt="10" x="83" y="-5"/>
+ <delta pt="11" x="-17" y="-3"/>
+ <delta pt="12" x="82" y="6"/>
+ <delta pt="13" x="84" y="3"/>
+ <delta pt="14" x="82" y="0"/>
+ <delta pt="15" x="81" y="0"/>
+ <delta pt="16" x="85" y="0"/>
+ <delta pt="17" x="82" y="-10"/>
+ <delta pt="18" x="80" y="-7"/>
+ <delta pt="19" x="-42" y="-7"/>
+ <delta pt="20" x="-44" y="-6"/>
+ <delta pt="21" x="-44" y="0"/>
+ <delta pt="22" x="-41" y="0"/>
+ <delta pt="23" x="-39" y="0"/>
+ <delta pt="24" x="-44" y="1"/>
+ <delta pt="25" x="-43" y="5"/>
+ <delta pt="26" x="57" y="3"/>
+ <delta pt="27" x="-42" y="-6"/>
+ <delta pt="28" x="-44" y="-4"/>
+ <delta pt="29" x="-43" y="0"/>
+ <delta pt="30" x="0" y="0"/>
+ <delta pt="31" x="0" y="0"/>
+ <delta pt="32" x="0" y="0"/>
+ <delta pt="33" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="O">
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="-43" y="0"/>
+ <delta pt="1" x="-45" y="0"/>
+ <delta pt="2" x="-36" y="2"/>
+ <delta pt="3" x="-19" y="6"/>
+ <delta pt="4" x="-8" y="6"/>
+ <delta pt="5" x="51" y="6"/>
+ <delta pt="6" x="62" y="5"/>
+ <delta pt="7" x="76" y="2"/>
+ <delta pt="8" x="84" y="0"/>
+ <delta pt="9" x="83" y="0"/>
+ <delta pt="10" x="85" y="0"/>
+ <delta pt="11" x="77" y="-2"/>
+ <delta pt="12" x="60" y="-5"/>
+ <delta pt="13" x="49" y="-6"/>
+ <delta pt="14" x="-10" y="-7"/>
+ <delta pt="15" x="-20" y="-5"/>
+ <delta pt="16" x="-36" y="-2"/>
+ <delta pt="17" x="-44" y="0"/>
+ <delta pt="18" x="-43" y="0"/>
+ <delta pt="19" x="-42" y="0"/>
+ <delta pt="20" x="-31" y="2"/>
+ <delta pt="21" x="-16" y="4"/>
+ <delta pt="22" x="-8" y="6"/>
+ <delta pt="23" x="51" y="6"/>
+ <delta pt="24" x="58" y="5"/>
+ <delta pt="25" x="72" y="2"/>
+ <delta pt="26" x="82" y="0"/>
+ <delta pt="27" x="83" y="0"/>
+ <delta pt="28" x="81" y="0"/>
+ <delta pt="29" x="71" y="-2"/>
+ <delta pt="30" x="57" y="-5"/>
+ <delta pt="31" x="49" y="-6"/>
+ <delta pt="32" x="-10" y="-7"/>
+ <delta pt="33" x="-17" y="-6"/>
+ <delta pt="34" x="-31" y="-2"/>
+ <delta pt="35" x="-42" y="0"/>
+ <delta pt="36" x="0" y="0"/>
+ <delta pt="37" x="0" y="0"/>
+ <delta pt="38" x="0" y="0"/>
+ <delta pt="39" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="Odieresis">
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="40" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="n">
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="-41" y="0"/>
+ <delta pt="1" x="-43" y="0"/>
+ <delta pt="2" x="-44" y="4"/>
+ <delta pt="3" x="-40" y="7"/>
+ <delta pt="4" x="40" y="4"/>
+ <delta pt="5" x="46" y="0"/>
+ <delta pt="6" x="43" y="0"/>
+ <delta pt="7" x="40" y="0"/>
+ <delta pt="8" x="41" y="0"/>
+ <delta pt="9" x="37" y="-3"/>
+ <delta pt="10" x="27" y="-6"/>
+ <delta pt="11" x="19" y="-6"/>
+ <delta pt="12" x="-42" y="-6"/>
+ <delta pt="13" x="-44" y="-5"/>
+ <delta pt="14" x="-43" y="0"/>
+ <delta pt="15" x="-41" y="0"/>
+ <delta pt="16" x="-43" y="0"/>
+ <delta pt="17" x="-44" y="4"/>
+ <delta pt="18" x="-40" y="7"/>
+ <delta pt="19" x="21" y="7"/>
+ <delta pt="20" x="26" y="4"/>
+ <delta pt="21" x="38" y="0"/>
+ <delta pt="22" x="40" y="0"/>
+ <delta pt="23" x="34" y="0"/>
+ <delta pt="24" x="-42" y="-6"/>
+ <delta pt="25" x="-44" y="-4"/>
+ <delta pt="26" x="-43" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ <delta pt="28" x="0" y="0"/>
+ <delta pt="29" x="0" y="0"/>
+ <delta pt="30" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="o">
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="-42" y="0"/>
+ <delta pt="1" x="-44" y="0"/>
+ <delta pt="2" x="-38" y="2"/>
+ <delta pt="3" x="-24" y="6"/>
+ <delta pt="4" x="-14" y="7"/>
+ <delta pt="5" x="18" y="7"/>
+ <delta pt="6" x="28" y="6"/>
+ <delta pt="7" x="41" y="3"/>
+ <delta pt="8" x="45" y="0"/>
+ <delta pt="9" x="44" y="0"/>
+ <delta pt="10" x="45" y="0"/>
+ <delta pt="11" x="40" y="-2"/>
+ <delta pt="12" x="27" y="-5"/>
+ <delta pt="13" x="16" y="-7"/>
+ <delta pt="14" x="-16" y="-7"/>
+ <delta pt="15" x="-26" y="-5"/>
+ <delta pt="16" x="-39" y="-3"/>
+ <delta pt="17" x="-44" y="0"/>
+ <delta pt="18" x="-42" y="0"/>
+ <delta pt="19" x="-40" y="0"/>
+ <delta pt="20" x="-24" y="4"/>
+ <delta pt="21" x="-14" y="7"/>
+ <delta pt="22" x="18" y="7"/>
+ <delta pt="23" x="27" y="5"/>
+ <delta pt="24" x="42" y="0"/>
+ <delta pt="25" x="44" y="0"/>
+ <delta pt="26" x="42" y="0"/>
+ <delta pt="27" x="27" y="-4"/>
+ <delta pt="28" x="16" y="-7"/>
+ <delta pt="29" x="-16" y="-7"/>
+ <delta pt="30" x="-25" y="-5"/>
+ <delta pt="31" x="-40" y="0"/>
+ <delta pt="32" x="0" y="0"/>
+ <delta pt="33" x="0" y="0"/>
+ <delta pt="34" x="0" y="0"/>
+ <delta pt="35" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="odieresis">
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="uni0308">
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="75" y="0"/>
+ <delta pt="1" x="73" y="0"/>
+ <delta pt="2" x="73" y="5"/>
+ <delta pt="3" x="76" y="6"/>
+ <delta pt="4" x="82" y="5"/>
+ <delta pt="5" x="84" y="3"/>
+ <delta pt="6" x="82" y="0"/>
+ <delta pt="7" x="81" y="0"/>
+ <delta pt="8" x="82" y="0"/>
+ <delta pt="9" x="83" y="-6"/>
+ <delta pt="10" x="80" y="-6"/>
+ <delta pt="11" x="74" y="-5"/>
+ <delta pt="12" x="72" y="-4"/>
+ <delta pt="13" x="73" y="0"/>
+ <delta pt="14" x="75" y="0"/>
+ <delta pt="15" x="73" y="0"/>
+ <delta pt="16" x="73" y="5"/>
+ <delta pt="17" x="76" y="6"/>
+ <delta pt="18" x="82" y="5"/>
+ <delta pt="19" x="84" y="3"/>
+ <delta pt="20" x="82" y="0"/>
+ <delta pt="21" x="81" y="0"/>
+ <delta pt="22" x="82" y="0"/>
+ <delta pt="23" x="83" y="-6"/>
+ <delta pt="24" x="80" y="-6"/>
+ <delta pt="25" x="74" y="-5"/>
+ <delta pt="26" x="72" y="-4"/>
+ <delta pt="27" x="73" y="0"/>
+ <delta pt="28" x="0" y="0"/>
+ <delta pt="29" x="0" y="0"/>
+ <delta pt="30" x="0" y="0"/>
+ <delta pt="31" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ </gvar>
+
+</ttFont>
diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx
index 33ebbd4a..66c80326 100644
--- a/Tests/varLib/data/test_results/BuildMain.ttx
+++ b/Tests/varLib/data/test_results/BuildMain.ttx
@@ -782,7 +782,7 @@
</MVAR>
<STAT>
- <Version value="0x00010002"/>
+ <Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=2 -->
<DesignAxisRecord>
diff --git a/Tests/varLib/data/test_results/BuildTestCFF2.ttx b/Tests/varLib/data/test_results/BuildTestCFF2.ttx
new file mode 100644
index 00000000..a4e859f1
--- /dev/null
+++ b/Tests/varLib/data/test_results/BuildTestCFF2.ttx
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="OTTO" ttLibVersion="3.32">
+
+ <fvar>
+
+ <!-- Weight -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>200.0</MinValue>
+ <DefaultValue>400.0</DefaultValue>
+ <MaxValue>900.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- ExtraLight -->
+ <!-- PostScript: TestCFF2Roman-ExtraLight -->
+ <NamedInstance flags="0x0" postscriptNameID="258" subfamilyNameID="257">
+ <coord axis="wght" value="200.0"/>
+ </NamedInstance>
+
+ <!-- Light -->
+ <!-- PostScript: TestCFF2Roman-Light -->
+ <NamedInstance flags="0x0" postscriptNameID="260" subfamilyNameID="259">
+ <coord axis="wght" value="300.0"/>
+ </NamedInstance>
+
+ <!-- Regular -->
+ <!-- PostScript: TestCFF2Roman-Regular -->
+ <NamedInstance flags="0x0" postscriptNameID="262" subfamilyNameID="261">
+ <coord axis="wght" value="400.0"/>
+ </NamedInstance>
+
+ <!-- Medium -->
+ <!-- PostScript: TestCFF2Roman-Medium -->
+ <NamedInstance flags="0x0" postscriptNameID="264" subfamilyNameID="263">
+ <coord axis="wght" value="500.0"/>
+ </NamedInstance>
+
+ <!-- Semibold -->
+ <!-- PostScript: TestCFF2Roman-Semibold -->
+ <NamedInstance flags="0x0" postscriptNameID="266" subfamilyNameID="265">
+ <coord axis="wght" value="600.0"/>
+ </NamedInstance>
+
+ <!-- Bold -->
+ <!-- PostScript: TestCFF2Roman-Bold -->
+ <NamedInstance flags="0x0" postscriptNameID="268" subfamilyNameID="267">
+ <coord axis="wght" value="700.0"/>
+ </NamedInstance>
+
+ <!-- Black -->
+ <!-- PostScript: TestCFF2Roman-Black -->
+ <NamedInstance flags="0x0" postscriptNameID="270" subfamilyNameID="269">
+ <coord axis="wght" value="900.0"/>
+ </NamedInstance>
+ </fvar>
+
+ <CFF2>
+ <major value="2"/>
+ <minor value="0"/>
+ <CFFFont name="CFF2Font">
+ <FontMatrix value="0.001 0 0 0.001 0 0"/>
+ <FDArray>
+ <FontDict index="0">
+ <Private>
+ <BlueValues>
+ <blend value="-12 0 0"/>
+ <blend value="0 0 0"/>
+ <blend value="486 -8 14"/>
+ <blend value="498 0 0"/>
+ <blend value="574 4 -8"/>
+ <blend value="586 0 0"/>
+ <blend value="638 6 -10"/>
+ <blend value="650 0 0"/>
+ <blend value="656 2 -2"/>
+ <blend value="668 0 0"/>
+ <blend value="712 6 -10"/>
+ <blend value="724 0 0"/>
+ </BlueValues>
+ <OtherBlues>
+ <blend value="-217 -17 29"/>
+ <blend value="-205 0 0"/>
+ </OtherBlues>
+ <BlueScale value="0.0625"/>
+ <BlueShift value="7"/>
+ <BlueFuzz value="0"/>
+ <StdHW>
+ <blend value="67 -39.0 67.0"/>
+ </StdHW>
+ <StdVW>
+ <blend value="85 -51.0 87.0"/>
+ </StdVW>
+ </Private>
+ </FontDict>
+ </FDArray>
+ <CharStrings>
+ <CharString name=".notdef">
+ 62 22 -38 1 blend
+ hmoveto
+ 476 -44 76 1 blend
+ 660 -476 44 -76 1 blend
+ -660 hlineto
+ 109 59 -61 103 -27 45 2 blend
+ rmoveto
+ 73 131 54 102 29 -47 45 -75 10 -18 4 -6 4 blend
+ 4 0 52 -102 73 -131 10 -16 -4 6 27 -47 -45 75 4 blend
+ rlineto
+ -256 -76 128 1 blend
+ hlineto
+ -44 52 34 -56 -10 16 2 blend
+ rmoveto
+ 461 75 -125 1 blend
+ vlineto
+ 127 -232 -127 -229 27 -45 -38 64 -27 45 -37 61 4 blend
+ rlineto
+ 171 277 5 -9 15 -25 2 blend
+ rmoveto
+ -50 93 -66 119 234 -6 10 -1 3 -28 48 49 -83 68 -114 5 blend
+ 0 -65 -119 -49 -93 -29 47 -49 83 -5 9 1 -3 4 blend
+ rlineto
+ -4 hlineto
+ 48 -48 -22 36 22 -36 2 blend
+ rmoveto
+ 126 232 26 -44 38 -64 2 blend
+ 0 -461 -126 229 -75 125 -26 44 37 -61 3 blend
+ rlineto
+ </CharString>
+ <CharString name="A">
+ 31 19 -31 1 blend
+ hmoveto
+ 86 -54 90 1 blend
+ hlineto
+ 115 366 23 73 21 72 21 76 25 -42 30 -50 5 -9 7 -11 3 -4 -4 6 3 -7 6 -10 8 blend
+ rlinecurve
+ 4 hlineto
+ 20 -76 22 -72 23 -73 4 -6 -6 10 2 -3 4 -6 5 -9 -7 11 6 blend
+ rrcurveto
+ 113 -366 90 25 -40 -30 50 -56 92 3 blend
+ 0 -221 656 -96 -15 25 4 -6 68 -112 3 blend
+ 0 -221 -656 -15 25 -4 6 2 blend
+ rlineto
+ 117 199 -15 24 37 -61 2 blend
+ rmoveto
+ 301 68 -301 -68 -8 15 -40 65 8 -15 40 -65 4 blend
+ hlineto
+ </CharString>
+ <CharString name="T">
+ 258 26 -44 1 blend
+ hmoveto
+ 84 585 217 71 -518 -71 217 -585 -52 88 47 -79 17 -30 -43 73 18 -28 43 -73 17 -30 -47 79 8 blend
+ hlineto
+ </CharString>
+ <CharString name="dollar">
+ 248 35 -3 12 -28 4 2 blend
+ rmoveto
+ -39 -45 5 18 -46 -26 -26 6 17 10 6 32 6 0 -3 5 blend
+ hvcurveto
+ 53 -36 -17 76 -17 36 -12 -17 -11 2 24 13 4 blend
+ rlineto
+ 53 -12 -22 13 -24 -37 -1 8 3 10 0 -9 5 13 -19 5 blend
+ hhcurveto
+ -22 -14 -11 -20 -9 8 -4 6 -13 4 -3 6 -18 8 -5 5 blend
+ hvcurveto
+ -87 4 81 -59 107 2 -3 20 -4 -20 -10 8 5 0 32 5 blend
+ hhcurveto
+ 136 82 76 107 82 -41 65 -135 47 -45 27 8 17 -23 8 4 10 -12 16 15 -17 1 3 1 -7 10 -2 9 blend
+ hvcurveto
+ -38 13 19 5 -5 -3 2 blend
+ rlineto
+ -71 23 -40 35 64 -22 -1 16 -1 -2 16 14 -11 4 -15 5 blend
+ vvcurveto
+ 75 57 37 74 30 36 -5 -17 42 16 -14 3 -10 11 -14 14 -7 26 12 -1 -9 -9 1 -33 -7 2 10 9 blend
+ vhcurveto
+ -52 36 17 -76 14 -33 11 11 11 -7 -24 9 4 blend
+ rlineto
+ -52 12 25 -14 22 37 -23 -6 -1 -15 12 9 0 -11 17 5 blend
+ hhcurveto
+ 19 17 10 21 8 -5 7 -9 12 -3 5 -7 20 -7 -3 5 blend
+ hvcurveto
+ 86 -6 -80 60 -101 2 2 -18 -2 13 4 -12 -12 17 -20 5 blend
+ hhcurveto
+ -115 -83 -80 -102 -100 62 -54 105 -37 23 -43 1 -2 29 0 -6 -13 20 7 -17 4 1 -15 -13 16 -5 -2 9 blend
+ hvcurveto
+ 37 -13 0 -5 -4 2 2 blend
+ rlineto
+ 85 -30 36 -30 -63 29 -5 -22 2 -10 -13 -16 11 -2 10 5 blend
+ vvcurveto
+ -74 -53 -42 -82 -18 19 -12 10 -12 3 -8 10 4 blend
+ vhcurveto
+ 31 287 -13 33 40 -12 2 blend
+ rmoveto
+ 428 -40 -428 40 0 -11 18 -31 0 11 -18 31 4 blend
+ vlineto
+ -41 -437 19 -38 -12 8 2 blend
+ rmoveto
+ 40 437 -40 -437 -18 31 12 -8 18 -31 -12 8 4 blend
+ hlineto
+ </CharString>
+ <CharString name="glyph00003">
+ 304 7 -12 1 blend
+ 34 rmoveto
+ 125 86 65 96 -22 38 2 -3 -9 15 -2 4 4 blend
+ hvcurveto
+ 183 -324 -21 110 1 -1 -14 22 -11 17 32 -54 4 blend
+ vvcurveto
+ 50 42 32 67 68 36 -21 -36 47 18 -29 15 -24 12 -21 18 -31 8 -13 -2 3 -3 5 -2 4 -3 5 9 blend
+ vhcurveto
+ 44 49 -24 40 -29 49 2 blend
+ rlineto
+ 44 -46 -54 33 -89 -6 8 5 -7 9 -15 -1 3 4 -8 5 blend
+ hhcurveto
+ -115 -81 -59 -94 16 -26 3 -7 5 -9 6 -10 4 blend
+ hvcurveto
+ -174 324 22 -124 8 -14 14 -22 6 -10 -32 56 4 blend
+ vvcurveto
+ -51 -42 -35 -78 -76 -62 31 37 -52 -19 31 -14 23 -15 25 -25 41 -9 15 -4 7 7 -11 -3 3 12 -20 9 blend
+ vhcurveto
+ -39 -58 21 -35 36 -58 2 blend
+ rlineto
+ -43 52 84 -36 83 5 -11 -7 13 -11 17 -4 8 8 -13 5 blend
+ hhcurveto
+ -51 -147 -19 32 1 -3 2 blend
+ rmoveto
+ 159 857 -56 7 -159 -858 56 -6 -1 1 3 -3 26 -44 -3 5 1 -1 -2 4 -26 44 2 -6 8 blend
+ rlineto
+ </CharString>
+ </CharStrings>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=1 -->
+ <!-- RegionCount=2 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=0 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=2 -->
+ <VarRegionIndex index="0" value="0"/>
+ <VarRegionIndex index="1" value="1"/>
+ </VarData>
+ </VarStore>
+ </CFFFont>
+
+ <GlobalSubrs>
+ <!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
+ </GlobalSubrs>
+ </CFF2>
+
+</ttFont>
diff --git a/Tests/varLib/data/test_results/FeatureVars.ttx b/Tests/varLib/data/test_results/FeatureVars.ttx
index 0764bb84..f2f6b05d 100644
--- a/Tests/varLib/data/test_results/FeatureVars.ttx
+++ b/Tests/varLib/data/test_results/FeatureVars.ttx
@@ -95,7 +95,7 @@
</ConditionTable>
</ConditionSet>
<FeatureTableSubstitution>
- <Version value="0x00010001"/>
+ <Version value="0x00010000"/>
<!-- SubstitutionCount=1 -->
<SubstitutionRecord index="0">
<FeatureIndex value="0"/>
@@ -109,21 +109,26 @@
</FeatureVariationRecord>
<FeatureVariationRecord index="1">
<ConditionSet>
- <!-- ConditionCount=1 -->
+ <!-- ConditionCount=2 -->
<ConditionTable index="0" Format="1">
+ <AxisIndex value="1"/>
+ <FilterRangeMinValue value="0.0"/>
+ <FilterRangeMaxValue value="0.25"/>
+ </ConditionTable>
+ <ConditionTable index="1" Format="1">
<AxisIndex value="0"/>
- <FilterRangeMinValue value="0.20886"/>
- <FilterRangeMaxValue value="1.0"/>
+ <FilterRangeMinValue value="-1.0"/>
+ <FilterRangeMaxValue value="-0.45654"/>
</ConditionTable>
</ConditionSet>
<FeatureTableSubstitution>
- <Version value="0x00010001"/>
+ <Version value="0x00010000"/>
<!-- SubstitutionCount=1 -->
<SubstitutionRecord index="0">
<FeatureIndex value="0"/>
<Feature>
<!-- LookupCount=1 -->
- <LookupListIndex index="0" value="0"/>
+ <LookupListIndex index="0" value="2"/>
</Feature>
</SubstitutionRecord>
</FeatureTableSubstitution>
@@ -138,7 +143,7 @@
</ConditionTable>
</ConditionSet>
<FeatureTableSubstitution>
- <Version value="0x00010001"/>
+ <Version value="0x00010000"/>
<!-- SubstitutionCount=1 -->
<SubstitutionRecord index="0">
<FeatureIndex value="0"/>
@@ -151,26 +156,21 @@
</FeatureVariationRecord>
<FeatureVariationRecord index="3">
<ConditionSet>
- <!-- ConditionCount=2 -->
+ <!-- ConditionCount=1 -->
<ConditionTable index="0" Format="1">
- <AxisIndex value="1"/>
- <FilterRangeMinValue value="0.0"/>
- <FilterRangeMaxValue value="0.25"/>
- </ConditionTable>
- <ConditionTable index="1" Format="1">
<AxisIndex value="0"/>
- <FilterRangeMinValue value="-1.0"/>
- <FilterRangeMaxValue value="-0.45654"/>
+ <FilterRangeMinValue value="0.20886"/>
+ <FilterRangeMaxValue value="1.0"/>
</ConditionTable>
</ConditionSet>
<FeatureTableSubstitution>
- <Version value="0x00010001"/>
+ <Version value="0x00010000"/>
<!-- SubstitutionCount=1 -->
<SubstitutionRecord index="0">
<FeatureIndex value="0"/>
<Feature>
<!-- LookupCount=1 -->
- <LookupListIndex index="0" value="2"/>
+ <LookupListIndex index="0" value="0"/>
</Feature>
</SubstitutionRecord>
</FeatureTableSubstitution>
diff --git a/Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx b/Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx
new file mode 100644
index 00000000..e2d0f71c
--- /dev/null
+++ b/Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="OTTO" ttLibVersion="3.32">
+
+ <hmtx>
+ <mtx name=".notdef" width="600" lsb="84"/>
+ <mtx name="A" width="600" lsb="50"/>
+ <mtx name="T" width="600" lsb="50"/>
+ <mtx name="dollar" width="600" lsb="102"/>
+ <mtx name="glyph00003" width="600" lsb="102"/>
+ </hmtx>
+
+ <CFF2>
+ <major value="2"/>
+ <minor value="0"/>
+ <CFFFont name="CFF2Font">
+ <FontMatrix value="0.001 0 0 0.001 0 0"/>
+ <FDArray>
+ <FontDict index="0">
+ <Private>
+ <BlueValues value="-12 0 478 490 570 582 640 652 660 672 722 734"/>
+ <OtherBlues value="-234 -222"/>
+ <BlueScale value="0.0625"/>
+ <BlueShift value="7"/>
+ <BlueFuzz value="0"/>
+ <StdHW value="28"/>
+ <StdVW value="34"/>
+ </Private>
+ </FontDict>
+ </FDArray>
+ <CharStrings>
+ <CharString name=".notdef">
+ 84 hmoveto
+ 432 660 -432 hlineto
+ 48 -628 rmoveto
+ 102 176 64 106 rlineto
+ 4 hlineto
+ 62 -106 100 -176 rlineto
+ -342 42 rmoveto
+ 536 vlineto
+ 154 -270 rlineto
+ 22 26 rmoveto
+ -56 92 -94 168 rlineto
+ 302 hlineto
+ -94 -168 -54 -92 rlineto
+ 22 -26 rmoveto
+ 152 270 rlineto
+ -536 vlineto
+ </CharString>
+ <CharString name="A">
+ 50 hmoveto
+ 32 hlineto
+ 140 396 28 80 24 68 24 82 rlinecurve
+ 4 hlineto
+ 24 -82 24 -68 28 -80 138 -396 rcurveline
+ 34 hlineto
+ -236 660 rlineto
+ -28 hlineto
+ -134 -424 rmoveto
+ 293 28 -293 hlineto
+ </CharString>
+ <CharString name="T">
+ 284 hmoveto
+ 32 632 234 28 -500 -28 234 hlineto
+ </CharString>
+ <CharString name="dollar">
+ 311 34 rmoveto
+ 103 88 56 94 hvcurveto
+ 184 -338 -32 142 vvcurveto
+ 68 57 44 85 76 34 -24 -38 44 vhcurveto
+ 20 20 rlineto
+ 38 -41 -45 32 -85 hhcurveto
+ -99 -78 -54 -88 hvcurveto
+ -166 338 28 -156 vvcurveto
+ -70 -56 -50 -103 -85 -66 38 34 -40 vhcurveto
+ -18 -22 45 -38 73 -40 91 0 rlinecurve
+ -18 566 rmoveto
+ 30 hlineto
+ 50 0 50 50 vvcurveto
+ -30 hlineto
+ -50 0 -50 -50 vvcurveto
+ -562 vmoveto
+ -148 30 148 vlineto
+ </CharString>
+ <CharString name="glyph00003">
+ 311 34 rmoveto
+ 103 88 56 94 hvcurveto
+ 184 -338 -32 142 vvcurveto
+ 68 57 44 85 76 34 -24 -38 44 vhcurveto
+ 20 20 rlineto
+ 38 -41 -45 32 -85 hhcurveto
+ -99 -78 -54 -88 hvcurveto
+ -166 338 28 -156 vvcurveto
+ -70 -56 -50 -103 -85 -66 38 34 -40 vhcurveto
+ -18 -22 rlineto
+ -38 45 73 -40 91 hhcurveto
+ -70 -146 rmoveto
+ 158 860 -30 4 -158 -860 rlineto
+ </CharString>
+ </CharStrings>
+ </CFFFont>
+
+ <GlobalSubrs>
+ <!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
+ </GlobalSubrs>
+ </CFF2>
+
+</ttFont>
diff --git a/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx b/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx
new file mode 100644
index 00000000..a20f0e4f
--- /dev/null
+++ b/Tests/varLib/data/test_results/Mutator_Getvar-instance.ttx
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.32">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="NULL"/>
+ <GlyphID id="2" name="nonmarkingreturn"/>
+ <GlyphID id="3" name="space"/>
+ <GlyphID id="4" name="b"/>
+ <GlyphID id="5" name="q"/>
+ <GlyphID id="6" name="a"/>
+ </GlyphOrder>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="750"/>
+ <descent value="-250"/>
+ <lineGap value="9"/>
+ <advanceWidthMax value="464"/>
+ <minLeftSideBearing value="38"/>
+ <minRightSideBearing value="38"/>
+ <xMaxExtent value="426"/>
+ <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="5"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="7"/>
+ <maxPoints value="20"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="20"/>
+ <maxCompositeContours value="2"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="1"/>
+ <maxStackElements value="2"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="1"/>
+ <maxComponentDepth value="1"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="347"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="3"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="700"/>
+ <ySubscriptYSize value="650"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="140"/>
+ <ySuperscriptXSize value="700"/>
+ <ySuperscriptYSize value="650"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="477"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="250"/>
+ <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 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="LuFo"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="0"/>
+ <usLastCharIndex value="113"/>
+ <sTypoAscender value="750"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="608"/>
+ <usWinDescent value="152"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="456"/>
+ <sCapHeight value="608"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="200" lsb="0"/>
+ <mtx name="NULL" width="0" lsb="0"/>
+ <mtx name="a" width="464" lsb="38"/>
+ <mtx name="b" width="464" lsb="76"/>
+ <mtx name="nonmarkingreturn" width="200" lsb="0"/>
+ <mtx name="q" width="464" lsb="38"/>
+ <mtx name="space" width="200" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x0" name="NULL"/><!-- ???? -->
+ <map code="0xd" name="nonmarkingreturn"/><!-- ???? -->
+ <map code="0x20" name="space"/><!-- SPACE -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x62" name="b"/><!-- LATIN SMALL LETTER B -->
+ <map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q -->
+ </cmap_format_4>
+ <cmap_format_6 platformID="1" platEncID="0" language="0">
+ <map code="0x0" name="NULL"/>
+ <map code="0xd" name="nonmarkingreturn"/>
+ <map code="0x20" name="space"/>
+ <map code="0x61" name="a"/>
+ <map code="0x62" name="b"/>
+ <map code="0x71" name="q"/>
+ </cmap_format_6>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x0" name="NULL"/><!-- ???? -->
+ <map code="0xd" name="nonmarkingreturn"/><!-- ???? -->
+ <map code="0x20" name="space"/><!-- SPACE -->
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x62" name="b"/><!-- LATIN SMALL LETTER B -->
+ <map code="0x71" name="q"/><!-- LATIN SMALL LETTER Q -->
+ </cmap_format_4>
+ </cmap>
+
+ <fpgm>
+ <assembly>
+ PUSHB[ ] /* 1 value pushed */
+ 145
+ IDEF[ ] /* InstructionDefinition */
+ NPUSHW[ ] /* 3 values pushed */
+ 2 -8192 8192
+ ENDF[ ] /* EndFunctionDefinition */
+ </assembly>
+ </fpgm>
+
+ <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"/><!-- contains no outline data -->
+
+ <TTGlyph name="NULL"/><!-- contains no outline data -->
+
+ <TTGlyph name="a" xMin="38" yMin="-12" xMax="388" yMax="468">
+ <contour>
+ <pt x="312" y="0" on="1"/>
+ <pt x="312" y="64" on="1"/>
+ <pt x="244" y="-12" on="1"/>
+ <pt x="180" y="-12" on="1"/>
+ <pt x="38" y="140" on="1"/>
+ <pt x="38" y="316" on="1"/>
+ <pt x="180" y="468" on="1"/>
+ <pt x="246" y="468" on="1"/>
+ <pt x="312" y="392" on="1"/>
+ <pt x="312" y="456" on="1"/>
+ <pt x="388" y="456" on="1"/>
+ <pt x="388" y="0" on="1"/>
+ </contour>
+ <contour>
+ <pt x="236" y="64" on="1"/>
+ <pt x="312" y="140" on="1"/>
+ <pt x="312" y="316" on="1"/>
+ <pt x="236" y="392" on="1"/>
+ <pt x="160" y="392" on="1"/>
+ <pt x="84" y="316" on="1"/>
+ <pt x="84" y="140" on="1"/>
+ <pt x="160" y="64" on="1"/>
+ </contour>
+ <instructions>
+ <assembly>
+ GETVARIATION[ ] /* GetVariation */
+ </assembly>
+ </instructions>
+ </TTGlyph>
+
+ <TTGlyph name="b" xMin="76" yMin="-12" xMax="426" yMax="628">
+ <contour>
+ <pt x="218" y="468" on="1"/>
+ <pt x="284" y="468" on="1"/>
+ <pt x="426" y="316" on="1"/>
+ <pt x="426" y="140" on="1"/>
+ <pt x="284" y="-12" on="1"/>
+ <pt x="220" y="-12" on="1"/>
+ <pt x="152" y="64" on="1"/>
+ <pt x="152" y="0" on="1"/>
+ <pt x="76" y="0" on="1"/>
+ <pt x="76" y="628" on="1"/>
+ <pt x="152" y="628" on="1"/>
+ <pt x="152" y="392" on="1"/>
+ </contour>
+ <contour>
+ <pt x="152" y="316" on="1"/>
+ <pt x="152" y="140" on="1"/>
+ <pt x="218" y="64" on="1"/>
+ <pt x="284" y="64" on="1"/>
+ <pt x="350" y="140" on="1"/>
+ <pt x="350" y="316" on="1"/>
+ <pt x="284" y="392" on="1"/>
+ <pt x="218" y="392" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="nonmarkingreturn"/><!-- contains no outline data -->
+
+ <TTGlyph name="q" xMin="38" yMin="-172" xMax="388" yMax="468">
+ <component glyphName="b" x="464" y="456" scale="-0.99994" flags="0x4"/>
+ </TTGlyph>
+
+ <TTGlyph name="space"/><!-- contains no outline data -->
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ VarFont
+ </namerecord>
+ <namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ VarFont Regular: 2017
+ </namerecord>
+ <namerecord nameID="4" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ VarFont Regular
+ </namerecord>
+ <namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ VarFont-Regular
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ VarFont
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ VarFont Regular: 2017
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ VarFont Regular
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ VarFont-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="NULL"/>
+ </extraNames>
+ </post>
+
+</ttFont>
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 100755..100644
--- 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/featureVars_test.py b/Tests/varLib/featureVars_test.py
new file mode 100644
index 00000000..4db2e625
--- /dev/null
+++ b/Tests/varLib/featureVars_test.py
@@ -0,0 +1,123 @@
+from __future__ import print_function, division, absolute_import
+from fontTools.misc.py23 import *
+from fontTools.varLib.featureVars import (
+ overlayFeatureVariations)
+
+
+def test_linear(n = 10):
+ conds = []
+ for i in range(n):
+ end = i / n
+ start = end - 1.
+ region = [{'X': (start, end)}]
+ subst = {'g%.2g'%start: 'g%.2g'%end}
+ conds.append((region, subst))
+ overlaps = overlayFeatureVariations(conds)
+ assert len(overlaps) == 2 * n - 1, overlaps
+ return conds, overlaps
+
+def test_quadratic(n = 10):
+ conds = []
+ for i in range(1, n + 1):
+ region = [{'X': (0, i / n),
+ 'Y': (0, (n + 1 - i) / n)}]
+ subst = {str(i): str(n + 1 - i)}
+ conds.append((region, subst))
+ overlaps = overlayFeatureVariations(conds)
+ assert len(overlaps) == n * (n + 1) // 2, overlaps
+ return conds, overlaps
+
+def _merge_substitutions(substitutions):
+ merged = {}
+ for subst in substitutions:
+ merged.update(subst)
+ return merged
+
+def _match_condition(location, overlaps):
+ for box, substitutions in overlaps:
+ for tag, coord in location.items():
+ start, end = box[tag]
+ if start <= coord <= end:
+ return _merge_substitutions(substitutions)
+ return {} # no match
+
+def test_overlaps_1():
+ # https://github.com/fonttools/fonttools/issues/1400
+ conds = [
+ ([{'abcd': (4, 9)}], {0: 0}),
+ ([{'abcd': (5, 10)}], {1: 1}),
+ ([{'abcd': (0, 8)}], {2: 2}),
+ ([{'abcd': (3, 7)}], {3: 3}),
+ ]
+ overlaps = overlayFeatureVariations(conds)
+ subst = _match_condition({'abcd': 0}, overlaps)
+ assert subst == {2: 2}
+ subst = _match_condition({'abcd': 1}, overlaps)
+ assert subst == {2: 2}
+ subst = _match_condition({'abcd': 3}, overlaps)
+ assert subst == {2: 2, 3: 3}
+ subst = _match_condition({'abcd': 4}, overlaps)
+ assert subst == {0: 0, 2: 2, 3: 3}
+ subst = _match_condition({'abcd': 5}, overlaps)
+ assert subst == {0: 0, 1: 1, 2: 2, 3: 3}
+ subst = _match_condition({'abcd': 7}, overlaps)
+ assert subst == {0: 0, 1: 1, 2: 2, 3: 3}
+ subst = _match_condition({'abcd': 8}, overlaps)
+ assert subst == {0: 0, 1: 1, 2: 2}
+ subst = _match_condition({'abcd': 9}, overlaps)
+ assert subst == {0: 0, 1: 1}
+ subst = _match_condition({'abcd': 10}, overlaps)
+ assert subst == {1: 1}
+
+def test_overlaps_2():
+ # https://github.com/fonttools/fonttools/issues/1400
+ conds = [
+ ([{'abcd': (1, 9)}], {0: 0}),
+ ([{'abcd': (8, 10)}], {1: 1}),
+ ([{'abcd': (3, 4)}], {2: 2}),
+ ([{'abcd': (1, 10)}], {3: 3}),
+ ]
+ overlaps = overlayFeatureVariations(conds)
+ subst = _match_condition({'abcd': 0}, overlaps)
+ assert subst == {}
+ subst = _match_condition({'abcd': 1}, overlaps)
+ assert subst == {0: 0, 3: 3}
+ subst = _match_condition({'abcd': 2}, overlaps)
+ assert subst == {0: 0, 3: 3}
+ subst = _match_condition({'abcd': 3}, overlaps)
+ assert subst == {0: 0, 2: 2, 3: 3}
+ subst = _match_condition({'abcd': 5}, overlaps)
+ assert subst == {0: 0, 3: 3}
+ subst = _match_condition({'abcd': 10}, overlaps)
+ assert subst == {1: 1, 3: 3}
+
+
+def run(test, n, quiet):
+
+ print()
+ print("%s:" % test.__name__)
+ input, output = test(n)
+ if quiet:
+ print(len(output))
+ else:
+ print()
+ print("Input:")
+ pprint(input)
+ print()
+ print("Output:")
+ pprint(output)
+ print()
+
+if __name__ == "__main__":
+ import sys
+ from pprint import pprint
+ quiet = False
+ n = 3
+ if len(sys.argv) > 1 and sys.argv[1] == '-q':
+ quiet = True
+ del sys.argv[1]
+ if len(sys.argv) > 1:
+ n = int(sys.argv[1])
+
+ run(test_linear, n=n, quiet=quiet)
+ run(test_quadratic, n=n, quiet=quiet)
diff --git a/Tests/varLib/mutator_test.py b/Tests/varLib/mutator_test.py
index de794f0f..8625de3c 100644
--- a/Tests/varLib/mutator_test.py
+++ b/Tests/varLib/mutator_test.py
@@ -3,6 +3,7 @@ from fontTools.misc.py23 import *
from fontTools.ttLib import TTFont
from fontTools.varLib import build
from fontTools.varLib.mutator import main as mutator
+from fontTools.varLib.mutator import instantiateVariableFont as make_instance
import difflib
import os
import shutil
@@ -117,6 +118,27 @@ class MutatorTest(unittest.TestCase):
expected_ttx_path = self.get_test_output(varfont_name + '.ttx')
self.expect_ttx(instfont, expected_ttx_path, tables)
+ def test_varlib_mutator_getvar_ttf(self):
+ suffix = '.ttf'
+ ttx_dir = self.get_test_input('master_ttx_getvar_ttf')
+
+ self.temp_dir()
+ ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'Mutator_Getvar')
+ for path in ttx_paths:
+ self.compile_font(path, suffix, self.tempdir)
+
+ varfont_name = 'Mutator_Getvar'
+ varfont_path = os.path.join(self.tempdir, varfont_name + suffix)
+
+ args = [varfont_path, 'wdth=80', 'ASCN=628']
+ mutator(args)
+
+ instfont_path = os.path.splitext(varfont_path)[0] + '-instance' + suffix
+ instfont = TTFont(instfont_path)
+ tables = [table_tag for table_tag in instfont.keys() if table_tag != 'head']
+ expected_ttx_path = self.get_test_output(varfont_name + '-instance.ttx')
+ self.expect_ttx(instfont, expected_ttx_path, tables)
+
def test_varlib_mutator_iup_ttf(self):
suffix = '.ttf'
ufo_dir = self.get_test_input('master_ufo')
@@ -139,6 +161,18 @@ class MutatorTest(unittest.TestCase):
expected_ttx_path = self.get_test_output(varfont_name + '-instance.ttx')
self.expect_ttx(instfont, expected_ttx_path, tables)
+ def test_varlib_mutator_CFF2(self):
+
+ otf_vf_path = self.get_test_input('TestCFF2VF.otf')
+ expected_ttx_name = 'InterpolateTestCFF2VF'
+ tables = ["hmtx", "CFF2"]
+ loc = {'wght':float(200)}
+
+ varfont = TTFont(otf_vf_path)
+ new_font = make_instance(varfont, loc)
+ expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
+ self.expect_ttx(new_font, expected_ttx_path, tables)
+
if __name__ == "__main__":
sys.exit(unittest.main())
diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py
index 2bfe0f2d..6638ea36 100644
--- a/Tests/varLib/varLib_test.py
+++ b/Tests/varLib/varLib_test.py
@@ -2,14 +2,25 @@ from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from fontTools.ttLib import TTFont
from fontTools.varLib import build
-from fontTools.varLib import main as varLib_main
-from fontTools.designspaceLib import DesignSpaceDocumentError
+from fontTools.varLib import main as varLib_main, load_masters
+from fontTools.designspaceLib import (
+ DesignSpaceDocumentError, DesignSpaceDocument, SourceDescriptor,
+)
import difflib
import os
import shutil
import sys
import tempfile
import unittest
+import pytest
+
+
+def reload_font(font):
+ """(De)serialize to get final binary layout."""
+ buf = BytesIO()
+ font.save(buf)
+ buf.seek(0)
+ return TTFont(buf)
class BuildTest(unittest.TestCase):
@@ -113,10 +124,7 @@ class BuildTest(unittest.TestCase):
# some data (e.g. counts printed in TTX inline comments) is only
# calculated at compile time, so before we can compare the TTX
# dumps we need to save to a temporary stream, and realod the font
- buf = BytesIO()
- varfont.save(buf)
- buf.seek(0)
- varfont = TTFont(buf)
+ varfont = reload_font(varfont)
expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
self.expect_ttx(varfont, expected_ttx_path, tables)
@@ -160,7 +168,7 @@ class BuildTest(unittest.TestCase):
avar segment will not be empty but will contain the default axis value
maps: {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}.
- This is to to work around an issue with some rasterizers:
+ This is to work around an issue with some rasterizers:
https://github.com/googlei18n/fontmake/issues/295
https://github.com/fonttools/fonttools/issues/1011
"""
@@ -180,7 +188,7 @@ class BuildTest(unittest.TestCase):
resulting avar segment still contains the default axis value maps:
{-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}.
- This is again to to work around an issue with some rasterizers:
+ This is again to work around an issue with some rasterizers:
https://github.com/googlei18n/fontmake/issues/295
https://github.com/fonttools/fonttools/issues/1011
"""
@@ -204,6 +212,38 @@ class BuildTest(unittest.TestCase):
save_before_dump=True,
)
+ def test_varlib_gvar_explicit_delta(self):
+ """The variable font contains a composite glyph odieresis which does not
+ need a gvar entry, because all its deltas are 0, but it must be added
+ anyway to work around an issue with macOS 10.14.
+
+ https://github.com/fonttools/fonttools/issues/1381
+ """
+ test_name = 'BuildGvarCompositeExplicitDelta'
+ self._run_varlib_build_test(
+ designspace_name=test_name,
+ font_name='TestFamily4',
+ tables=['gvar'],
+ expected_ttx_name=test_name
+ )
+
+ def test_varlib_build_CFF2(self):
+ ds_path = self.get_test_input('TestCFF2.designspace')
+ suffix = '.otf'
+ expected_ttx_name = 'BuildTestCFF2'
+ tables = ["fvar", "CFF2"]
+
+ finder = lambda s: s.replace('.ufo', suffix)
+ varfont, model, _ = build(ds_path, finder)
+ # some data (e.g. counts printed in TTX inline comments) is only
+ # calculated at compile time, so before we can compare the TTX
+ # dumps we need to save to a temporary stream, and realod the font
+ varfont = reload_font(varfont)
+
+ expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
+ self.expect_ttx(varfont, expected_ttx_path, tables)
+ self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
+
def test_varlib_main_ttf(self):
"""Mostly for testing varLib.main()
"""
@@ -250,6 +290,44 @@ 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):
+ 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:
+ filename = os.path.join(
+ self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf")
+ )
+ source.font = TTFont(
+ filename, recalcBBoxes=False, recalcTimestamp=False, lazy=True
+ )
+ source.filename = None # Make sure no file path gets into build()
+
+ 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_load_masters_layerName_without_required_font():
+ ds = DesignSpaceDocument()
+ s = SourceDescriptor()
+ s.font = None
+ s.layerName = "Medium"
+ ds.addSource(s)
+
+ with pytest.raises(
+ AttributeError,
+ match="specified a layer name but lacks the required TTFont object",
+ ):
+ load_masters(ds)
+
if __name__ == "__main__":
sys.exit(unittest.main())
diff --git a/fonttools b/fonttools
index 92b390e7..92b390e7 100755..100644
--- a/fonttools
+++ b/fonttools
diff --git a/post_update.sh b/post_update.sh
new file mode 100755
index 00000000..7ea7d90a
--- /dev/null
+++ b/post_update.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+# $1 Path to the new version.
+# $2 Path to the old version.
+
+cp -a -n $2/Lib/fontTools/Android.bp $1/Lib/fontTools/
diff --git a/requirements.txt b/requirements.txt
index 0e5f5bab..c9ac4f39 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,9 @@
# we use the official Brotli module on CPython and the CFFI-based
# extension 'brotlipy' on PyPy
-brotli==1.0.1; platform_python_implementation != "PyPy"
+brotli==1.0.7; platform_python_implementation != "PyPy"
brotlipy==0.7.0; platform_python_implementation == "PyPy"
unicodedata2==11.0.0; python_version < '3.7' and platform_python_implementation != "PyPy"
-scipy==1.1.0; platform_python_implementation != "PyPy"
+scipy==1.2.0; platform_python_implementation != "PyPy"
munkres==1.0.12; platform_python_implementation == "PyPy"
-zopfli==0.1.4
-fs==2.1.1
+zopfli==0.1.6
+fs==2.1.3
diff --git a/run-tests.sh b/run-tests.sh
index f10c1b01..f10c1b01 100755..100644
--- a/run-tests.sh
+++ b/run-tests.sh
diff --git a/setup.cfg b/setup.cfg
index 0d36f4c8..6ee25421 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 3.31.0
+current_version = 3.35.0
commit = True
tag = False
tag_name = {new_version}
@@ -37,7 +37,6 @@ license_file = LICENSE
minversion = 3.0
testpaths =
Tests
- fontTools
python_files =
*_test.py
python_classes =
@@ -47,6 +46,9 @@ addopts =
--doctest-modules
--doctest-ignore-import-errors
--pyargs
+doctest_optionflags =
+ ALLOW_UNICODE
+ ELLIPSIS
filterwarnings =
ignore:tostring:DeprecationWarning
ignore:fromstring:DeprecationWarning
diff --git a/setup.py b/setup.py
index e975f63d..25093cd4 100755..100644
--- a/setup.py
+++ b/setup.py
@@ -39,6 +39,11 @@ extras_require = {
"lxml": [
"lxml >= 4.0, < 5",
"singledispatch >= 3.4.0.3; python_version < '3.4'",
+ # typing >= 3.6.4 is required when using ABC collections with the
+ # singledispatch backport, see:
+ # https://github.com/fonttools/fonttools/issues/1423
+ # https://github.com/python/typing/issues/484
+ "typing >= 3.6.4; python_version < '3.4'",
],
# for fontTools.sfnt and fontTools.woff2: to compress/uncompress
# WOFF 1.0 and WOFF 2.0 webfonts.
@@ -58,6 +63,10 @@ extras_require = {
"python_version < '3.7' and platform_python_implementation != 'PyPy'"
),
],
+ # for graphite type tables in ttLib/tables (Silf, Glat, Gloc)
+ "graphite": [
+ "lz4 >= 1.7.4.2"
+ ],
# for fontTools.interpolatable: to solve the "minimum weight perfect
# matching problem in bipartite graphs" (aka Assignment problem)
"interpolatable": [
@@ -65,6 +74,12 @@ extras_require = {
"scipy; platform_python_implementation != 'PyPy'",
"munkres; platform_python_implementation == 'PyPy'",
],
+ # for fontTools.varLib.plot, to visualize DesignSpaceDocument and resulting
+ # VariationModel
+ "plot": [
+ # TODO: figure out the minimum version of matplotlib that we need
+ "matplotlib",
+ ],
# for fontTools.misc.symfont, module for symbolic font statistics analysis
"symfont": [
"sympy",
@@ -337,7 +352,7 @@ def find_data_files(manpath="share/man"):
setup(
name="fonttools",
- version="3.31.0",
+ version="3.35.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",
diff --git a/tox.ini b/tox.ini
index 63619e3d..2e8d9ee0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -15,8 +15,8 @@ extras =
!nolxml: lxml
commands =
# test with or without coverage, passing extra positonal args to pytest
- cov: coverage run --parallel-mode -m pytest {posargs}
- !cov: pytest {posargs}
+ cov: coverage run --parallel-mode -m pytest {posargs:Tests fontTools}
+ !cov: pytest {posargs:Tests fontTools}
[testenv:htmlcov]
deps =
@@ -37,10 +37,17 @@ commands =
coverage combine
codecov --env TOXENV
+[testenv:package_readme]
+description = check that the long description is valid (need for PyPi)
+deps = twine >= 1.12.1
+ pip >= 18.0.0
+skip_install = true
+extras =
+commands = pip wheel -w {envtmpdir}/build --no-deps .
+ twine check {envtmpdir}/build/*
+
[testenv:bdist]
deps =
- pygments
- docutils
setuptools
wheel
skip_install = true
@@ -50,8 +57,6 @@ install_command =
whitelist_externals =
rm
commands =
- # check metadata and rst long_description
- python setup.py check --restructuredtext --strict
# clean up build/ and dist/ folders
python -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)'
python setup.py clean --all