diff --git a/Lib/fontTools/cffLib/CFF2ToCFF.py b/Lib/fontTools/cffLib/CFF2ToCFF.py
new file mode 100644
index 000000000..5dc48a7fc
--- /dev/null
+++ b/Lib/fontTools/cffLib/CFF2ToCFF.py
@@ -0,0 +1,165 @@
+"""CFF2 to CFF converter."""
+
+from fontTools.ttLib import TTFont, newTable
+from fontTools.misc.cliTools import makeOutputFileName
+from fontTools.cffLib import TopDictIndex, buildOrder, topDictOperators
+from .width import optimizeWidths
+from collections import defaultdict
+import logging
+
+
+__all__ = ["convertCFF2ToCFF", "main"]
+
+
+log = logging.getLogger("fontTools.cffLib")
+
+
+def _convertCFF2ToCFF(cff, otFont):
+ """Converts this object from CFF2 format to CFF format. This conversion
+ is done 'in-place'. The conversion cannot be reversed.
+
+ The CFF2 font cannot be variable. (TODO Accept those and convert to the
+ default instance?)
+
+ This assumes a decompiled CFF table. (i.e. that the object has been
+ filled via :meth:`decompile` and e.g. not loaded from XML.)"""
+
+ cff.major = 1
+
+ topDictData = TopDictIndex(None, isCFF2=True)
+ for item in cff.topDictIndex:
+ # Iterate over, such that all are decompiled
+ topDictData.append(item)
+ cff.topDictIndex = topDictData
+ topDict = topDictData[0]
+
+ if hasattr(topDict, "VarStore"):
+ raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
+
+ if hasattr(topDict, "Private"):
+ privateDict = topDict.Private
+ else:
+ privateDict = None
+ opOrder = buildOrder(topDictOperators)
+ topDict.order = opOrder
+
+ fdArray = topDict.FDArray
+ charStrings = topDict.CharStrings
+
+ for cs in charStrings.values():
+ cs.decompile()
+ cs.program.append("endchar")
+ for subrSets in [cff.GlobalSubrs] + [
+ getattr(fd.Private, "Subrs", []) for fd in fdArray
+ ]:
+ for cs in subrSets:
+ cs.program.append("return")
+
+ # Add (optimal) width to CharStrings that need it.
+ widths = defaultdict(list)
+ metrics = otFont["hmtx"].metrics
+ for glyphName in charStrings.keys():
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
+ if fdIndex == None:
+ fdIndex = 0
+ widths[fdIndex].append(metrics[glyphName][0])
+ for fdIndex, widthList in widths.items():
+ bestDefault, bestNominal = optimizeWidths(widthList)
+ private = fdArray[fdIndex].Private
+ private.defaultWidthX = bestDefault
+ private.nominalWidthX = bestNominal
+ for glyphName in charStrings.keys():
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
+ if fdIndex == None:
+ fdIndex = 0
+ private = fdArray[fdIndex].Private
+ width = metrics[glyphName][0]
+ if width != private.defaultWidthX:
+ cs.program.insert(0, width - private.nominalWidthX)
+
+
+def convertCFF2ToCFF(font, *, updatePostTable=True):
+ cff = font["CFF2"].cff
+ _convertCFF2ToCFF(cff, font)
+ del font["CFF2"]
+ table = font["CFF "] = newTable("CFF ")
+ table.cff = cff
+
+ if updatePostTable and "post" in font:
+ # Only version supported for fonts with CFF table is 0x00030000 not 0x20000
+ post = font["post"]
+ if post.formatType == 2.0:
+ post.formatType = 3.0
+
+
+def main(args=None):
+ """Convert CFF OTF font to CFF2 OTF font"""
+ if args is None:
+ import sys
+
+ args = sys.argv[1:]
+
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ "fonttools cffLib.CFFToCFF2",
+ description="Upgrade a CFF font to CFF2.",
+ )
+ parser.add_argument(
+ "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
+ )
+ parser.add_argument(
+ "-o",
+ "--output",
+ metavar="OUTPUT.ttf",
+ default=None,
+ help="Output instance OTF file (default: INPUT-CFF2.ttf).",
+ )
+ parser.add_argument(
+ "--no-recalc-timestamp",
+ dest="recalc_timestamp",
+ action="store_false",
+ help="Don't set the output font's timestamp to the current time.",
+ )
+ loggingGroup = parser.add_mutually_exclusive_group(required=False)
+ loggingGroup.add_argument(
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
+ )
+ loggingGroup.add_argument(
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
+ )
+ options = parser.parse_args(args)
+
+ from fontTools import configLogger
+
+ configLogger(
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
+ )
+
+ import os
+
+ infile = options.input
+ if not os.path.isfile(infile):
+ parser.error("No such file '{}'".format(infile))
+
+ outfile = (
+ makeOutputFileName(infile, overWrite=True, suffix="-CFF")
+ if not options.output
+ else options.output
+ )
+
+ font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
+
+ convertCFF2ToCFF(font)
+
+ log.info(
+ "Saving %s",
+ outfile,
+ )
+ font.save(outfile)
+
+
+if __name__ == "__main__":
+ import sys
+
+ sys.exit(main(sys.argv[1:]))
diff --git a/Lib/fontTools/cffLib/CFFToCFF2.py b/Lib/fontTools/cffLib/CFFToCFF2.py
new file mode 100644
index 000000000..804af8477
--- /dev/null
+++ b/Lib/fontTools/cffLib/CFFToCFF2.py
@@ -0,0 +1,273 @@
+"""CFF to CFF2 converter."""
+
+from fontTools.ttLib import TTFont, newTable
+from fontTools.misc.cliTools import makeOutputFileName
+from fontTools.misc.psCharStrings import T2WidthExtractor
+from fontTools.cffLib import (
+ TopDictIndex,
+ FDArrayIndex,
+ FontDict,
+ buildOrder,
+ topDictOperators,
+ privateDictOperators,
+ topDictOperators2,
+ privateDictOperators2,
+)
+from io import BytesIO
+import logging
+
+__all__ = ["convertCFFToCFF2", "main"]
+
+
+log = logging.getLogger("fontTools.cffLib")
+
+
+class _NominalWidthUsedError(Exception):
+ def __add__(self, other):
+ raise self
+
+ def __radd__(self, other):
+ raise self
+
+
+def _convertCFFToCFF2(cff, otFont):
+ """Converts this object from CFF format to CFF2 format. This conversion
+ is done 'in-place'. The conversion cannot be reversed.
+
+ This assumes a decompiled CFF table. (i.e. that the object has been
+ filled via :meth:`decompile` and e.g. not loaded from XML.)"""
+
+ # Clean up T2CharStrings
+
+ topDict = cff.topDictIndex[0]
+ fdArray = topDict.FDArray if hasattr(topDict, "FDArray") else None
+ charStrings = topDict.CharStrings
+ globalSubrs = cff.GlobalSubrs
+ localSubrs = [getattr(fd.Private, "Subrs", []) for fd in fdArray] if fdArray else []
+
+ for glyphName in charStrings.keys():
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
+ cs.decompile()
+
+ # Clean up subroutines first
+ for subrs in [globalSubrs] + localSubrs:
+ for subr in subrs:
+ program = subr.program
+ i = j = len(program)
+ try:
+ i = program.index("return")
+ except ValueError:
+ pass
+ try:
+ j = program.index("endchar")
+ except ValueError:
+ pass
+ program[min(i, j) :] = []
+
+ # Clean up glyph charstrings
+ nominalWidthXError = _NominalWidthUsedError()
+ for glyphName in charStrings.keys():
+ cs, fdIndex = charStrings.getItemAndSelector(glyphName)
+ program = cs.program
+ if fdIndex == None:
+ fdIndex = 0
+
+ # Intentionally use custom type for nominalWidthX, such that any
+ # CharString that has an explicit width encoded will throw back to us.
+ extractor = T2WidthExtractor(
+ localSubrs[fdIndex] if localSubrs else [],
+ globalSubrs,
+ nominalWidthXError,
+ 0,
+ )
+ try:
+ extractor.execute(cs)
+ except _NominalWidthUsedError:
+ # Program has explicit width. We want to drop it, but can't
+ # just pop the first number since it may be a subroutine call.
+ # Instead, when seeing that, we embed the subroutine and recurse.
+ # This has the problem that some subroutines might become unused.
+ # We don't currently prune those. Subset module has code for this
+ # kind of stuff, possibly plug it in here if pruning becomes needed.
+ while program[1] in ["callsubr", "callgsubr"]:
+ subrNumber = program.pop(0)
+ op = program.pop(0)
+ bias = extractor.localBias if op == "callsubr" else extractor.globalBias
+ subrNumber += bias
+ subrSet = localSubrs[fdIndex] if op == "callsubr" else globalSubrs
+ subrProgram = subrSet[subrNumber].program
+ program[:0] = subrProgram
+ # Now pop the actual width
+ program.pop(0)
+
+ if program and program[-1] == "endchar":
+ program.pop()
+
+ # Upconvert TopDict
+
+ cff.major = 2
+ cff2GetGlyphOrder = cff.otFont.getGlyphOrder
+ topDictData = TopDictIndex(None, cff2GetGlyphOrder)
+ for item in cff.topDictIndex:
+ # Iterate over, such that all are decompiled
+ topDictData.append(item)
+ 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)
+ if privateDict is not None:
+ 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 list(privateDict.rawDict.keys()):
+ # 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 deprecated 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)
+
+ # TODO(behdad): What does the following comment even mean? Both CFF and CFF2
+ # use the same T2Charstring class. I *think* what it means is that the CharStrings
+ # were loaded for CFF1, and we need to reload them for CFF2 to set varstore, etc
+ # on them. At least that's what I understand. It's probably safe to remove this
+ # and just set vstore where needed.
+
+ # At this point, the Subrs and Charstrings are all still T2Charstring class
+ # easiest to fix this by compiling, then decompiling again
+ file = BytesIO()
+ cff.compile(file, otFont, isCFF2=True)
+ file.seek(0)
+ cff.decompile(file, otFont, isCFF2=True)
+
+
+def convertCFFToCFF2(font):
+ cff = font["CFF "].cff
+ del font["CFF "]
+ _convertCFFToCFF2(cff, font)
+ table = font["CFF2"] = newTable("CFF2")
+ table.cff = cff
+
+
+def main(args=None):
+ """Convert CFF OTF font to CFF2 OTF font"""
+ if args is None:
+ import sys
+
+ args = sys.argv[1:]
+
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ "fonttools cffLib.CFFToCFF2",
+ description="Upgrade a CFF font to CFF2.",
+ )
+ parser.add_argument(
+ "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
+ )
+ parser.add_argument(
+ "-o",
+ "--output",
+ metavar="OUTPUT.ttf",
+ default=None,
+ help="Output instance OTF file (default: INPUT-CFF2.ttf).",
+ )
+ parser.add_argument(
+ "--no-recalc-timestamp",
+ dest="recalc_timestamp",
+ action="store_false",
+ help="Don't set the output font's timestamp to the current time.",
+ )
+ loggingGroup = parser.add_mutually_exclusive_group(required=False)
+ loggingGroup.add_argument(
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
+ )
+ loggingGroup.add_argument(
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
+ )
+ options = parser.parse_args(args)
+
+ from fontTools import configLogger
+
+ configLogger(
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
+ )
+
+ import os
+
+ infile = options.input
+ if not os.path.isfile(infile):
+ parser.error("No such file '{}'".format(infile))
+
+ outfile = (
+ makeOutputFileName(infile, overWrite=True, suffix="-CFF2")
+ if not options.output
+ else options.output
+ )
+
+ font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
+
+ convertCFFToCFF2(font)
+
+ log.info(
+ "Saving %s",
+ outfile,
+ )
+ font.save(outfile)
+
+
+if __name__ == "__main__":
+ import sys
+
+ sys.exit(main(sys.argv[1:]))
diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py
index f1e78d24d..bd954f515 100644
--- a/Lib/fontTools/cffLib/__init__.py
+++ b/Lib/fontTools/cffLib/__init__.py
@@ -99,9 +99,6 @@ class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
desubroutinized = desubroutinized[
: desubroutinized.index("endchar") + 1
]
- else:
- if not len(desubroutinized) or desubroutinized[-1] != "return":
- desubroutinized.append("return")
charString._desubroutinized = desubroutinized
del charString._patches
@@ -389,91 +386,20 @@ class CFFFontSet(object):
self.minor = int(attrs["value"])
def convertCFFToCFF2(self, otFont):
- """Converts this object from CFF format to CFF2 format. This conversion
- is done 'in-place'. The conversion cannot be reversed.
+ from .CFFToCFF2 import _convertCFFToCFF2
- This assumes a decompiled CFF table. (i.e. that the object has been
- filled via :meth:`decompile`.)"""
- self.major = 2
- cff2GetGlyphOrder = self.otFont.getGlyphOrder
- topDictData = TopDictIndex(None, cff2GetGlyphOrder)
- topDictData.items = self.topDictIndex.items
- self.topDictIndex = topDictData
- topDict = topDictData[0]
- if hasattr(topDict, "Private"):
- privateDict = topDict.Private
- else:
- privateDict = None
- opOrder = buildOrder(topDictOperators2)
- topDict.order = opOrder
- topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
- 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)
+ _convertCFFToCFF2(self, otFont)
- 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 fontDict.rawDict.keys():
- if key not in fontDict.order:
- del fontDict.rawDict[key]
- if hasattr(fontDict, key):
- delattr(fontDict, key)
+ def convertCFF2ToCFF(self, otFont):
+ from .CFF2ToCFF import _convertCFF2ToCFF
- 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
- # At this point, the Subrs and Charstrings are all still T2Charstring class
- # easiest to fix this by compiling, then decompiling again
- file = BytesIO()
- self.compile(file, otFont, isCFF2=True)
- file.seek(0)
- self.decompile(file, otFont, isCFF2=True)
+ _convertCFF2ToCFF(self, otFont)
def desubroutinize(self):
for fontName in self.fontNames:
font = self[fontName]
cs = font.CharStrings
- for g in font.charset:
- c, _ = cs.getItemAndSelector(g)
+ for c in cs.values():
c.decompile()
subrs = getattr(c.private, "Subrs", [])
decompiler = _DesubroutinizingT2Decompiler(
@@ -764,8 +690,8 @@ class Index(object):
compilerClass = IndexCompiler
def __init__(self, file=None, isCFF2=None):
- assert (isCFF2 is None) == (file is None)
self.items = []
+ self.offsets = offsets = []
name = self.__class__.__name__
if file is None:
return
@@ -782,7 +708,6 @@ class Index(object):
offSize = readCard8(file)
log.log(DEBUG, " index count: %s offSize: %s", count, offSize)
assert offSize <= 4, "offSize too large: %s" % offSize
- self.offsets = offsets = []
pad = b"\0" * (4 - offSize)
for index in range(count + 1):
chunk = file.read(offSize)
@@ -960,7 +885,6 @@ class TopDictIndex(Index):
compilerClass = TopDictIndexCompiler
def __init__(self, file=None, cff2GetGlyphOrder=None, topSize=0, isCFF2=None):
- assert (isCFF2 is None) == (file is None)
self.cff2GetGlyphOrder = cff2GetGlyphOrder
if file is not None and isCFF2:
self._isCFF2 = isCFF2
@@ -2861,9 +2785,11 @@ class PrivateDict(BaseDict):
# Provide dummy values. This avoids needing to provide
# an isCFF2 state in a lot of places.
self.nominalWidthX = self.defaultWidthX = None
+ self._isCFF2 = True
else:
self.defaults = buildDefaults(privateDictOperators)
self.order = buildOrder(privateDictOperators)
+ self._isCFF2 = False
@property
def in_cff2(self):
diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py
index 39f51ed75..bb7f89e4f 100644
--- a/Lib/fontTools/cffLib/specializer.py
+++ b/Lib/fontTools/cffLib/specializer.py
@@ -43,10 +43,8 @@ def programToCommands(program, getNumRegions=None):
hintmask/cntrmask argument, as well as stray arguments at the end of the
program (🤷).
'getNumRegions' may be None, or a callable object. It must return the
- number of regions. 'getNumRegions' takes a single argument, vsindex. If
- the vsindex argument is None, getNumRegions returns the default number
- of regions for the charstring, else it returns the numRegions for
- the vsindex.
+ number of regions. 'getNumRegions' takes a single argument, vsindex. It
+ returns the numRegions for the vsindex.
The Charstring may or may not start with a width value. If the first
non-blend operator has an odd number of arguments, then the first argument is
a width, and is popped off. This is complicated with blend operators, as
@@ -61,7 +59,7 @@ def programToCommands(program, getNumRegions=None):
"""
seenWidthOp = False
- vsIndex = None
+ vsIndex = 0
lenBlendStack = 0
lastBlendIndex = 0
commands = []
diff --git a/Lib/fontTools/cffLib/width.py b/Lib/fontTools/cffLib/width.py
index 0ba3ed39b..78ff27e4f 100644
--- a/Lib/fontTools/cffLib/width.py
+++ b/Lib/fontTools/cffLib/width.py
@@ -13,6 +13,9 @@ from operator import add
from functools import reduce
+__all__ = ["optimizeWidths", "main"]
+
+
class missingdict(dict):
def __init__(self, missing_func):
self.missing_func = missing_func
diff --git a/Lib/fontTools/merge/tables.py b/Lib/fontTools/merge/tables.py
index 57ed64d33..d132cb2a0 100644
--- a/Lib/fontTools/merge/tables.py
+++ b/Lib/fontTools/merge/tables.py
@@ -294,6 +294,8 @@ def merge(self, m, tables):
extractor.execute(c)
width = extractor.width
if width is not defaultWidthXToken:
+ # The following will be wrong if the width is added
+ # by a subroutine. Ouch!
c.program.pop(0)
else:
width = defaultWidthX
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py
index 578f6328f..8a9f146b2 100644
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -537,7 +537,7 @@ class TTFont(object):
#
# Not enough names found in the 'post' table.
# Can happen when 'post' format 1 is improperly used on a font that
- # has more than 258 glyphs (the lenght of 'standardGlyphOrder').
+ # has more than 258 glyphs (the length of 'standardGlyphOrder').
#
log.warning(
"Not enough names found in the 'post' table, generating them from cmap instead"
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index 1e0f2ec2f..6d0e00ee1 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -845,9 +845,10 @@ def _add_CFF2(varFont, model, master_fonts):
glyphOrder = varFont.getGlyphOrder()
if "CFF2" not in varFont:
- from .cff import convertCFFtoCFF2
+ from fontTools.cffLib.CFFToCFF2 import convertCFFToCFF2
+
+ convertCFFToCFF2(varFont)
- convertCFFtoCFF2(varFont)
ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping)
# re-ordering the master list simplifies building the CFF2 data item lists.
merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
diff --git a/Lib/fontTools/varLib/cff.py b/Lib/fontTools/varLib/cff.py
index 52e6a8848..393c793e3 100644
--- a/Lib/fontTools/varLib/cff.py
+++ b/Lib/fontTools/varLib/cff.py
@@ -49,95 +49,6 @@ def addCFFVarStore(varFont, varModel, varDataList, masterSupports):
fontDict.Private.vstore = topDict.VarStore
-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)
- if privateDict is not None:
- 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 deprecated 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 "]
-
-
def conv_to_int(num):
if isinstance(num, float) and num.is_integer():
return int(num)
diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py
index 4c58a6143..7120b0831 100644
--- a/Lib/fontTools/varLib/instancer/__init__.py
+++ b/Lib/fontTools/varLib/instancer/__init__.py
@@ -89,7 +89,7 @@ from fontTools.misc.fixedTools import (
otRound,
)
from fontTools.varLib.models import normalizeValue, piecewiseLinearMap
-from fontTools.ttLib import TTFont
+from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables import _g_l_y_f
from fontTools import varLib
@@ -97,6 +97,13 @@ from fontTools import varLib
# we import the `subset` module because we use the `prune_lookups` method on the GSUB
# table class, and that method is only defined dynamically upon importing `subset`
from fontTools import subset # noqa: F401
+from fontTools.cffLib import privateDictOperators2
+from fontTools.cffLib.specializer import (
+ programToCommands,
+ commandsToProgram,
+ specializeCommands,
+ generalizeCommands,
+)
from fontTools.varLib import builder
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.merger import MutatorMerger
@@ -566,6 +573,259 @@ def changeTupleVariationAxisLimit(var, axisTag, axisLimit):
return out
+def instantiateCFF2(
+ varfont,
+ axisLimits,
+ *,
+ round=round,
+ specialize=True,
+ generalize=False,
+ downgrade=False,
+):
+ # The algorithm here is rather simple:
+ #
+ # Take all blend operations and store their deltas in the (otherwise empty)
+ # CFF2 VarStore. Then, instantiate the VarStore with the given axis limits,
+ # and read back the new deltas. This is done for both the CharStrings and
+ # the Private dicts.
+ #
+ # Then prune unused things and possibly drop the VarStore if it's empty.
+ # In which case, downgrade to CFF table if requested.
+
+ log.info("Instantiating CFF2 table")
+
+ fvarAxes = varfont["fvar"].axes
+
+ cff = varfont["CFF2"].cff
+ topDict = cff.topDictIndex[0]
+ varStore = topDict.VarStore.otVarStore
+ if not varStore:
+ if downgrade:
+ from fontTools.cffLib.CFF2ToCFF import convertCFF2ToCFF
+
+ convertCFF2ToCFF(varfont)
+ return
+
+ cff.desubroutinize()
+
+ def getNumRegions(vsindex):
+ return varStore.VarData[vsindex if vsindex is not None else 0].VarRegionCount
+
+ charStrings = topDict.CharStrings.values()
+
+ # Gather all unique private dicts
+ uniquePrivateDicts = set()
+ privateDicts = []
+ for fd in topDict.FDArray:
+ if fd.Private not in uniquePrivateDicts:
+ uniquePrivateDicts.add(fd.Private)
+ privateDicts.append(fd.Private)
+
+ allCommands = []
+ for cs in charStrings:
+ assert cs.private.vstore.otVarStore is varStore # Or in many places!!
+ commands = programToCommands(cs.program, getNumRegions=getNumRegions)
+ if generalize:
+ commands = generalizeCommands(commands)
+ if specialize:
+ commands = specializeCommands(commands, generalizeFirst=not generalize)
+ allCommands.append(commands)
+
+ def storeBlendsToVarStore(arg):
+ if not isinstance(arg, list):
+ return
+
+ if any(isinstance(subarg, list) for subarg in arg[:-1]):
+ raise NotImplementedError("Nested blend lists not supported (yet)")
+
+ count = arg[-1]
+ assert (len(arg) - 1) % count == 0
+ nRegions = (len(arg) - 1) // count - 1
+ assert nRegions == getNumRegions(vsindex)
+ for i in range(count, len(arg) - 1, nRegions):
+ deltas = arg[i : i + nRegions]
+ assert len(deltas) == nRegions
+ varData = varStore.VarData[vsindex]
+ varData.Item.append(deltas)
+ varData.ItemCount += 1
+
+ def fetchBlendsFromVarStore(arg):
+ if not isinstance(arg, list):
+ return [arg]
+
+ if any(isinstance(subarg, list) for subarg in arg[:-1]):
+ raise NotImplementedError("Nested blend lists not supported (yet)")
+
+ count = arg[-1]
+ assert (len(arg) - 1) % count == 0
+ numRegions = getNumRegions(vsindex)
+ newDefaults = []
+ newDeltas = []
+ for i in range(count):
+ defaultValue = arg[i]
+
+ major = vsindex
+ minor = varDataCursor[major]
+ varDataCursor[major] += 1
+
+ varIdx = (major << 16) + minor
+
+ defaultValue += round(defaultDeltas[varIdx])
+ newDefaults.append(defaultValue)
+
+ varData = varStore.VarData[major]
+ deltas = varData.Item[minor]
+ assert len(deltas) == numRegions
+ newDeltas.extend(deltas)
+
+ if not numRegions:
+ return newDefaults # No deltas, just return the defaults
+
+ return [newDefaults + newDeltas + [count]]
+
+ # Check VarData's are empty
+ for varData in varStore.VarData:
+ assert varData.Item == []
+ assert varData.ItemCount == 0
+
+ # Add charstring blend lists to VarStore so we can instantiate them
+ for commands in allCommands:
+ vsindex = 0
+ for command in commands:
+ if command[0] == "vsindex":
+ vsindex = command[1][0]
+ continue
+ for arg in command[1]:
+ storeBlendsToVarStore(arg)
+
+ # Add private blend lists to VarStore so we can instantiate values
+ vsindex = 0
+ for opcode, name, arg_type, default, converter in privateDictOperators2:
+ if arg_type not in ("number", "delta", "array"):
+ continue
+
+ vsindex = 0
+ for private in privateDicts:
+ if not hasattr(private, name):
+ continue
+ values = getattr(private, name)
+
+ if name == "vsindex":
+ vsindex = values[0]
+ continue
+
+ if arg_type == "number":
+ values = [values]
+
+ for value in values:
+ if not isinstance(value, list):
+ continue
+
+ assert len(value) % (getNumRegions(vsindex) + 1) == 0
+ count = len(value) // (getNumRegions(vsindex) + 1)
+ storeBlendsToVarStore(value + [count])
+
+ # Instantiate VarStore
+ defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
+
+ # Read back new charstring blends from the instantiated VarStore
+ varDataCursor = [0] * len(varStore.VarData)
+ for commands in allCommands:
+ vsindex = 0
+ for command in commands:
+ if command[0] == "vsindex":
+ vsindex = command[1][0]
+ continue
+ newArgs = []
+ for arg in command[1]:
+ newArgs.extend(fetchBlendsFromVarStore(arg))
+ command[1][:] = newArgs
+
+ # Read back new private blends from the instantiated VarStore
+ for opcode, name, arg_type, default, converter in privateDictOperators2:
+ if arg_type not in ("number", "delta", "array"):
+ continue
+
+ for private in privateDicts:
+ if not hasattr(private, name):
+ continue
+ values = getattr(private, name)
+ if arg_type == "number":
+ values = [values]
+
+ newValues = []
+ for value in values:
+ if not isinstance(value, list):
+ newValues.append(value)
+ continue
+
+ value.append(1)
+ value = fetchBlendsFromVarStore(value)
+ newValues.extend(v[:-1] if isinstance(v, list) else v for v in value)
+
+ if arg_type == "number":
+ newValues = newValues[0]
+
+ setattr(private, name, newValues)
+
+ # Empty out the VarStore
+ for i, varData in enumerate(varStore.VarData):
+ assert varDataCursor[i] == varData.ItemCount, (
+ varDataCursor[i],
+ varData.ItemCount,
+ )
+ varData.Item = []
+ varData.ItemCount = 0
+
+ # Remove vsindex commands that are no longer needed, collect those that are.
+ usedVsindex = set()
+ for commands in allCommands:
+ if any(isinstance(arg, list) for command in commands for arg in command[1]):
+ vsindex = 0
+ for command in commands:
+ if command[0] == "vsindex":
+ vsindex = command[1][0]
+ continue
+ if any(isinstance(arg, list) for arg in command[1]):
+ usedVsindex.add(vsindex)
+ else:
+ commands[:] = [command for command in commands if command[0] != "vsindex"]
+
+ # Remove unused VarData and update vsindex values
+ vsindexMapping = {v: i for i, v in enumerate(sorted(usedVsindex))}
+ varStore.VarData = [
+ varData for i, varData in enumerate(varStore.VarData) if i in usedVsindex
+ ]
+ varStore.VarDataCount = len(varStore.VarData)
+ for commands in allCommands:
+ for command in commands:
+ if command[0] == "vsindex":
+ command[1][0] = vsindexMapping[command[1][0]]
+
+ # Remove initial vsindex commands that are implied
+ for commands in allCommands:
+ if commands and commands[0] == ("vsindex", [0]):
+ commands.pop(0)
+
+ # Ship the charstrings!
+ for cs, commands in zip(charStrings, allCommands):
+ cs.program = commandsToProgram(commands)
+
+ # Remove empty VarStore
+ if not varStore.VarData:
+ if "VarStore" in topDict.rawDict:
+ del topDict.rawDict["VarStore"]
+ del topDict.VarStore
+ del topDict.CharStrings.varStore
+ for private in privateDicts:
+ del private.vstore
+
+ if downgrade:
+ from fontTools.cffLib.CFF2ToCFF import convertCFF2ToCFF
+
+ convertCFF2ToCFF(varfont)
+
+
def _instantiateGvarGlyph(
glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=True
):
@@ -765,22 +1025,57 @@ def _remapVarIdxMap(table, attrName, varIndexMapping, glyphOrder):
# TODO(anthrotype) Add support for HVAR/VVAR in CFF2
-def _instantiateVHVAR(varfont, axisLimits, tableFields):
+def _instantiateVHVAR(varfont, axisLimits, tableFields, *, round=round):
location = axisLimits.pinnedLocation()
tableTag = tableFields.tableTag
fvarAxes = varfont["fvar"].axes
- # Deltas from gvar table have already been applied to the hmtx/vmtx. For full
- # instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return
- if set(location).issuperset(axis.axisTag for axis in fvarAxes):
- log.info("Dropping %s table", tableTag)
- del varfont[tableTag]
- return
log.info("Instantiating %s table", tableTag)
vhvar = varfont[tableTag].table
varStore = vhvar.VarStore
- # since deltas were already applied, the return value here is ignored
- instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
+
+ if "glyf" in varfont:
+ # Deltas from gvar table have already been applied to the hmtx/vmtx. For full
+ # instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return
+ if set(location).issuperset(axis.axisTag for axis in fvarAxes):
+ log.info("Dropping %s table", tableTag)
+ del varfont[tableTag]
+ return
+
+ defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
+
+ if "glyf" not in varfont:
+ # CFF2 fonts need hmtx/vmtx updated here. For glyf fonts, the instantiateGvar
+ # function already updated the hmtx/vmtx from phantom points. Maybe remove
+ # that and do it here for both CFF2 and glyf fonts?
+ #
+ # Specially, if a font has glyf but not gvar, the hmtx/vmtx will not have been
+ # updated by instantiateGvar. Though one can call that a faulty font.
+ metricsTag = "vmtx" if tableTag == "VVAR" else "hmtx"
+ if metricsTag in varfont:
+ advMapping = getattr(vhvar, tableFields.advMapping)
+ metricsTable = varfont[metricsTag]
+ metrics = metricsTable.metrics
+ for glyphName, (advanceWidth, sb) in metrics.items():
+ if advMapping:
+ varIdx = advMapping.mapping[glyphName]
+ else:
+ varIdx = varfont.getGlyphID(glyphName)
+ metrics[glyphName] = (advanceWidth + round(defaultDeltas[varIdx]), sb)
+
+ if (
+ tableTag == "VVAR"
+ and getattr(vhvar, tableFields.vOrigMapping) is not None
+ ):
+ log.warning(
+ "VORG table not yet updated to reflect changes in VVAR table"
+ )
+
+ # For full instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return
+ if set(location).issuperset(axis.axisTag for axis in fvarAxes):
+ log.info("Dropping %s table", tableTag)
+ del varfont[tableTag]
+ return
if varStore.VarRegionList.Region:
# Only re-optimize VarStore if the HVAR/VVAR already uses indirect AdvWidthMap
@@ -923,6 +1218,8 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits):
newItemVarStore = tupleVarStore.asItemVarStore()
itemVarStore.VarRegionList = newItemVarStore.VarRegionList
+ if not hasattr(itemVarStore, "VarDataCount"): # Happens fromXML
+ itemVarStore.VarDataCount = len(newItemVarStore.VarData)
assert itemVarStore.VarDataCount == newItemVarStore.VarDataCount
itemVarStore.VarData = newItemVarStore.VarData
@@ -1222,9 +1519,6 @@ def sanityCheckVariableTables(varfont):
if "gvar" in varfont:
if "glyf" not in varfont:
raise ValueError("Can't have gvar without glyf")
- # TODO(anthrotype) Remove once we do support partial instancing CFF2
- if "CFF2" in varfont:
- raise NotImplementedError("Instancing CFF2 variable fonts is not supported yet")
def instantiateVariableFont(
@@ -1234,6 +1528,8 @@ def instantiateVariableFont(
optimize=True,
overlap=OverlapMode.KEEP_AND_SET_FLAGS,
updateFontNames=False,
+ *,
+ downgradeCFF2=False,
):
"""Instantiate variable font, either fully or partially.
@@ -1273,6 +1569,11 @@ def instantiateVariableFont(
in the head and OS/2 table will be updated so they conform to the R/I/B/BI
model. If the STAT table is missing or an Axis Value table is missing for
a given axis coordinate, a ValueError will be raised.
+ downgradeCFF2 (bool): if True, downgrade the CFF2 table to CFF table when possible
+ ie. full instancing of all axes. This is useful for compatibility with older
+ software that does not support CFF2. Defaults to False. Note that this
+ operation also removes overlaps within glyph shapes, as CFF does not support
+ overlaps but CFF2 does.
"""
# 'overlap' used to be bool and is now enum; for backward compat keep accepting bool
overlap = OverlapMode(int(overlap))
@@ -1297,6 +1598,9 @@ def instantiateVariableFont(
log.info("Updating name table")
names.updateNameTable(varfont, axisLimits)
+ if "CFF2" in varfont:
+ instantiateCFF2(varfont, normalizedLimits, downgrade=downgradeCFF2)
+
if "gvar" in varfont:
instantiateGvar(varfont, normalizedLimits, optimize=optimize)
@@ -1488,6 +1792,11 @@ def parseArgs(args):
help="Update the instantiated font's `name` table. Input font must have "
"a STAT table with Axis Value Tables",
)
+ parser.add_argument(
+ "--downgrade-cff2",
+ action="store_true",
+ help="If all axes are pinned, downgrade CFF2 to CFF table format",
+ )
parser.add_argument(
"--no-recalc-timestamp",
dest="recalc_timestamp",
@@ -1559,6 +1868,7 @@ def main(args=None):
optimize=options.optimize,
overlap=options.overlap,
updateFontNames=options.update_name_table,
+ downgradeCFF2=options.downgrade_cff2,
)
suffix = "-instance" if isFullInstance else "-partial"
diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py
index c831d02e8..0cbf5d0ce 100644
--- a/Tests/fontBuilder/fontBuilder_test.py
+++ b/Tests/fontBuilder/fontBuilder_test.py
@@ -330,9 +330,9 @@ def test_build_cff_to_cff2(tmpdir):
}
fb.setupCFF("TestFont", {}, charStrings, {})
- from fontTools.varLib.cff import convertCFFtoCFF2
+ from fontTools.cffLib.CFFToCFF2 import convertCFFToCFF2
- convertCFFtoCFF2(fb.font)
+ convertCFFToCFF2(fb.font)
def test_setupNameTable_no_mac():
diff --git a/Tests/varLib/instancer/data/CFF2Instancer-VF-1.ttx b/Tests/varLib/instancer/data/CFF2Instancer-VF-1.ttx
new file mode 100644
index 000000000..c140f2f37
--- /dev/null
+++ b/Tests/varLib/instancer/data/CFF2Instancer-VF-1.ttx
@@ -0,0 +1,729 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
+
+
+ Noto Sans SC
+
+
+ Regular
+
+
+ 2.004;ADBO;NotoSansSC-Thin;ADOBE
+
+
+ Noto Sans SC
+
+
+ Version 2.004;hotconv 1.0.118;makeotfexe 2.5.65603
+
+
+ NotoSansSC-Thin
+
+
+ Regular
+
+
+ Thin
+
+
+ Light
+
+
+ DemiLight
+
+
+ Regular
+
+
+ Medium
+
+
+ Bold
+
+
+ Black
+
+
+ Weight
+
+
+ Thin
+
+
+ NotoSansSC-Thin
+
+
+ Light
+
+
+ NotoSansSC-Light
+
+
+ DemiLight
+
+
+ NotoSansSC-DemiLight
+
+
+ Regular
+
+
+ NotoSansSC-Regular
+
+
+ Medium
+
+
+ NotoSansSC-Medium
+
+
+ Bold
+
+
+ NotoSansSC-Bold
+
+
+ Black
+
+
+ NotoSansSC-Black
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 44 256 6 -29 2 blend
+ rmoveto
+ 239 35 -239 44 90 -44 3 blend
+ hlineto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ wght
+ 0x0
+ 100.0
+ 100.0
+ 900.0
+ 265
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/varLib/instancer/data/CFF2Instancer-VF-2.ttx b/Tests/varLib/instancer/data/CFF2Instancer-VF-2.ttx
new file mode 100644
index 000000000..e26eba29d
--- /dev/null
+++ b/Tests/varLib/instancer/data/CFF2Instancer-VF-2.ttx
@@ -0,0 +1,615 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ License same as MutatorMath. BSD 3-clause. [test-token: C]
+
+
+ Regular
+
+
+ 1.002;LTTR;MutatorMathTest-LightCondensed
+
+
+ MutatorMathTest LightCondensed
+
+
+ Version 1.002
+
+
+ MutatorMathTest-LightCondensed
+
+
+ Width
+
+
+ Weight
+
+
+ width
+
+
+ weight
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1 blend
+ hlineto
+
+
+ 2 blend
+ rmoveto
+
+
+ 3 blend
+ hlineto
+
+
+ 1 blend
+ hmoveto
+
+
+ 0 100 0 2 blend
+ rlineto
+
+
+ 1 vsindex
+ 20 30 -30 0 -104 callsubr
+ 40 7 220 63 -107 callsubr
+ 140 700 375 -56 -169 -103 callsubr
+ -35 -7 -195 -43 -107 callsubr
+ -90 -536 -235 96 79 60 -144 -60 -106 callsubr
+ 250 36 -250 450 220 -190 -6 174 16 -450 -220 190 -105 callsubr
+ 257 -200 585 23 -275 -54 -130 44 -106 callsubr
+ 44 9 296 121 -107 callsubr
+ -145 700 -375 29 151 -103 callsubr
+ -39 -9 -281 -121 -107 callsubr
+ -17 -39 15 -73 5 3 -221 -3 -106 callsubr
+ 47 39 -47 3 223 147 -3 221 3 -3 -223 -147 -105 callsubr
+
+
+
+
+
+
+
+
+
+ -102 callsubr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ wdth
+ 0x0
+ 0.0
+ 0.0
+ 1000.0
+ 256
+
+
+
+
+ wght
+ 0x0
+ 100.0
+ 100.0
+ 900.0
+ 257
+
+
+
+
diff --git a/Tests/varLib/instancer/data/CFF2Instancer-VF-3.ttx b/Tests/varLib/instancer/data/CFF2Instancer-VF-3.ttx
new file mode 100644
index 000000000..55a009ed1
--- /dev/null
+++ b/Tests/varLib/instancer/data/CFF2Instancer-VF-3.ttx
@@ -0,0 +1,648 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
+
+
+ Noto Sans SC
+
+
+ Regular
+
+
+ 2.004;ADBO;NotoSansSC-Thin;ADOBE
+
+
+ Noto Sans SC
+
+
+ Version 2.004;hotconv 1.0.118;makeotfexe 2.5.65603
+
+
+ NotoSansSC-Thin
+
+
+ Regular
+
+
+ Thin
+
+
+ Light
+
+
+ DemiLight
+
+
+ Regular
+
+
+ Medium
+
+
+ Bold
+
+
+ Black
+
+
+ Weight
+
+
+ Thin
+
+
+ NotoSansSC-Thin
+
+
+ Light
+
+
+ NotoSansSC-Light
+
+
+ DemiLight
+
+
+ NotoSansSC-DemiLight
+
+
+ Regular
+
+
+ NotoSansSC-Regular
+
+
+ Medium
+
+
+ NotoSansSC-Medium
+
+
+ Bold
+
+
+ NotoSansSC-Bold
+
+
+ Black
+
+
+ NotoSansSC-Black
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 69 271 -16 33 2 blend
+ rmoveto
+ -30 856 30 -95 40 95 3 blend
+ vlineto
+ -443 103 -81 -61 2 blend
+ rmoveto
+ -446 30 446 5 119 -5 3 blend
+ vlineto
+ -62 -110 -125 -2 2 blend
+ rmoveto
+ -87 -119 -170 -109 -147 -49 7 -5 9 -11 5 -9 145 54 174 111 90 124 11 40 23 37 5 14 24 -25 35 -45 16 -26 1 -4 -29 -15 1 -12 18 blend
+ rrcurveto
+ 71 12 39 33 2 blend
+ rmoveto
+ -27 -13 -86 -39 2 blend
+ rlineto
+ 88 -127 173 -108 158 -50 5 9 9 10 7 6 -159 45 -170 104 -84 124 7 22 -25 14 -16 1 16 28 35 46 25 23 19 -10 19 -35 6 -50 18 blend
+ rrcurveto
+ -451 501 -120 18 2 blend
+ rmoveto
+ 42 -51 44 -70 16 -45 -11 5 -10 8 -3 5 6 blend
+ rrcurveto
+ 28 15 -17 44 -44 68 -42 50 86 50 3 -4 6 -9 9 -8 8 blend
+ rlinecurve
+ 183 56 -86 -8 2 blend
+ rmoveto
+ -472 28 472 -50 111 50 3 blend
+ vlineto
+ -273 -379 -84 31 2 blend
+ rmoveto
+ 16 -26 70 33 93 45 88 42 42 -101 -2 8 -13 5 -16 7 8 blend
+ rlinecurve
+ -7 26 -97 -46 -98 -46 -65 -32 87 9 -3 7 -3 5 7 blend
+ -28 rlinecurve
+ 467 173 19 -103 2 blend
+ rmoveto
+ 59 -44 67 -64 31 -43 -24 2 -24 5 -14 5 6 blend
+ rrcurveto
+ 26 17 -33 43 -67 63 -58 43 84 51 13 -5 22 -8 22 -4 8 blend
+ rlinecurve
+ 8 104 -63 31 2 blend
+ rmoveto
+ -29 340 29 -90 -29 90 3 blend
+ vlineto
+ -291 89 -3 -2 2 blend
+ rmoveto
+ -33 -81 -82 -84 -90 -50 7 -5 9 -11 4 -6 94 52 84 89 38 88 -6 12 -3 4 6 4 18 -19 29 -37 16 -23 3 6 14 7 25 9 18 blend
+ rrcurveto
+ 251 -81 -122 39 2 blend
+ rmoveto
+ -7 -14 1 blend
+ vlineto
+ -76 -215 -200 -89 -239 -37 6 -7 8 -12 4 -7 243 43 202 90 84 231 17 41 27 6 11 5 19 -20 30 -48 10 -27 -1 4 -4 21 -1 1 18 blend
+ rrcurveto
+ -17 12 -7 -2 -71 33 -17 -2 4 blend
+ rlineto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ wght
+ 0x0
+ 100.0
+ 100.0
+ 900.0
+ 265
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-1-instance-400.ttx b/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-1-instance-400.ttx
new file mode 100644
index 000000000..44237b80c
--- /dev/null
+++ b/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-1-instance-400.ttx
@@ -0,0 +1,476 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
+
+
+ Noto Sans SC
+
+
+ Regular
+
+
+ 2.004;ADBO;NotoSansSC-Thin;ADOBE
+
+
+ Noto Sans SC
+
+
+ Version 2.004;hotconv 1.0.118;makeotfexe 2.5.65603
+
+
+ NotoSansSC-Thin
+
+
+ Regular
+
+
+ Regular
+
+
+ Weight
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 46 245 rmoveto
+ 256 70 -256 hlineto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-2-instance-400.ttx b/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-2-instance-400.ttx
new file mode 100644
index 000000000..69d746ff6
--- /dev/null
+++ b/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-2-instance-400.ttx
@@ -0,0 +1,350 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ License same as MutatorMath. BSD 3-clause. [test-token: C]
+
+
+ Regular
+
+
+ 1.002;LTTR;MutatorMathTest-LightCondensed
+
+
+ MutatorMathTest LightCondensed
+
+
+ Version 1.002
+
+
+ MutatorMathTest-LightCondensed
+
+
+ Width
+
+
+ width
+
+
+ weight
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 9 30 1 blend
+ hmoveto
+ 122 31 1 blend
+ hlineto
+ 119 738 312 0 2 blend
+ rlineto
+ -108 -23 1 blend
+ hlineto
+ -54 -590 -205 38 2 blend
+ rmoveto
+ 332 101 -332 379 0 -379 3 blend
+ hlineto
+ 266 -249 482 -37 2 blend
+ rmoveto
+ 155 54 1 blend
+ hlineto
+ -134 738 -318 0 2 blend
+ rlineto
+ -144 -54 1 blend
+ hlineto
+ -44 -122 17 2 2 blend
+ rmoveto
+ 131 122 -131 58 -2 -58 3 blend
+ hlineto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ wdth
+ 0x0
+ 0.0
+ 0.0
+ 1000.0
+ 256
+
+
+
+
diff --git a/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-3-instance-400.ttx b/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-3-instance-400.ttx
new file mode 100644
index 000000000..138cb1cce
--- /dev/null
+++ b/Tests/varLib/instancer/data/test_results/CFF2Instancer-VF-3-instance-400.ttx
@@ -0,0 +1,377 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
+
+
+ Noto Sans SC
+
+
+ Regular
+
+
+ 2.004;ADBO;NotoSansSC-Thin;ADOBE
+
+
+ Noto Sans SC
+
+
+ Version 2.004;hotconv 1.0.118;makeotfexe 2.5.65603
+
+
+ NotoSansSC-Thin
+
+
+ Regular
+
+
+ Regular
+
+
+ Weight
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 63 284 rmoveto
+ -67 872 67 vlineto
+ -475 79 rmoveto
+ -444 76 444 vlineto
+ -111 -111 rmoveto
+ -83 -103 -161 -95 -145 -44 16 -15 23 -29 11 -19 145 52 163 105 90 119 rrcurveto
+ 86 25 rmoveto
+ -61 -28 rlineto
+ 91 -118 163 -103 152 -50 11 20 23 28 17 15 -152 41 -163 90 -82 104 rrcurveto
+ -498 508 rmoveto
+ 38 -49 40 -67 15 -43 rrcurveto
+ 62 35 -16 42 -42 64 -38 47 rlinecurve
+ 149 53 rmoveto
+ -492 71 492 vlineto
+ -306 -367 rmoveto
+ 32 -65 69 36 88 47 82 45 rlinecurve
+ -19 60 -93 -47 -95 -47 -63 -28 rlinecurve
+ 474 133 rmoveto
+ 50 -43 58 -62 26 -41 rrcurveto
+ 59 37 -28 41 -58 60 -49 41 rlinecurve
+ -17 116 rmoveto
+ -64 329 64 vlineto
+ -292 88 rmoveto
+ -35 -76 -83 -82 -88 -48 14 -12 20 -25 10 -15 95 54 89 92 48 92 rrcurveto
+ 203 -66 rmoveto
+ -12 vlineto
+ -69 -199 -189 -87 -235 -35 13 -15 20 -31 8 -18 243 45 200 98 84 231 rrcurveto
+ -45 25 -14 -3 rlineto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py
index 3dfbf448f..ca7ea93f0 100644
--- a/Tests/varLib/instancer/instancer_test.py
+++ b/Tests/varLib/instancer/instancer_test.py
@@ -63,6 +63,88 @@ def _get_coordinates(varfont, glyphname):
)
+class InstantiateCFF2Test(object):
+ @pytest.mark.parametrize(
+ "location, expected",
+ [
+ (
+ {},
+ [
+ 44,
+ 256,
+ 6,
+ -29,
+ 2,
+ "blend",
+ "rmoveto",
+ 239,
+ 35,
+ -239,
+ 44,
+ 90,
+ -44,
+ 3,
+ "blend",
+ "hlineto",
+ ],
+ ),
+ ({"wght": 0}, [44, 256, "rmoveto", 239, 35, -239, "hlineto"]),
+ ({"wght": 0.5}, [47, 242, "rmoveto", 261, 80, -261, "hlineto"]),
+ ({"wght": 1}, [50, 227, "rmoveto", 283, 125, -283, "hlineto"]),
+ ],
+ )
+ def test_pin_and_drop_axis(self, varfont, location, expected):
+
+ varfont = ttLib.TTFont()
+ varfont.importXML(os.path.join(TESTDATA, "CFF2Instancer-VF-1.ttx"))
+
+ location = instancer.NormalizedAxisLimits(location)
+
+ instancer.instantiateCFF2(varfont, location)
+ instancer.instantiateHVAR(varfont, location)
+
+ program = varfont["CFF2"].cff.topDictIndex[0].CharStrings.values()[1].program
+ assert program == expected
+
+ @pytest.mark.parametrize(
+ "source_ttx, expected_ttx",
+ [
+ ("CFF2Instancer-VF-1.ttx", "CFF2Instancer-VF-1-instance-400.ttx"),
+ ("CFF2Instancer-VF-2.ttx", "CFF2Instancer-VF-2-instance-400.ttx"),
+ ("CFF2Instancer-VF-3.ttx", "CFF2Instancer-VF-3-instance-400.ttx"),
+ ],
+ )
+ def test_full_instance(self, varfont, source_ttx, expected_ttx):
+ varfont = ttLib.TTFont()
+ varfont.importXML(os.path.join(TESTDATA, source_ttx))
+ s = BytesIO()
+ varfont.save(s)
+ s.seek(0)
+ varfont = ttLib.TTFont(s)
+
+ instance = instancer.instantiateVariableFont(varfont, {"wght": 400})
+ s = BytesIO()
+ instance.save(s)
+ s.seek(0)
+ instance = ttLib.TTFont(s)
+
+ s = StringIO()
+ instance.saveXML(s)
+ actual = stripVariableItemsFromTTX(s.getvalue())
+
+ expected = ttLib.TTFont()
+ expected.importXML(os.path.join(TESTDATA, "test_results", expected_ttx))
+ s = BytesIO()
+ expected.save(s)
+ s.seek(0)
+ expected = ttLib.TTFont(s)
+ s = StringIO()
+ expected.saveXML(s)
+ expected = stripVariableItemsFromTTX(s.getvalue())
+
+ assert actual == expected
+
+
class InstantiateGvarTest(object):
@pytest.mark.parametrize("glyph_name", ["hyphen"])
@pytest.mark.parametrize(