Merge pull request #1377 from fonttools/varlib-interpolate-cff2

Varlib interpolate cff2
This commit is contained in:
Behdad Esfahbod 2018-11-30 00:21:13 -05:00 committed by GitHub
commit 361e1e6aa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 868 additions and 559 deletions

View File

@ -1151,6 +1151,13 @@ class T2CharString(object):
class CFF2Subr(T2CharString):
isCFF2 = True
def draw(self, pen):
subrs = getattr(self.private, "Subrs", [])
extractor = self.outlineExtractor(pen, subrs, self.globalSubrs,
0, 0)
extractor.execute(self)
self.width = 0
class T1CharString(T2CharString):
operandEncoding = t1OperandEncoding

View File

@ -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
@ -2081,522 +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 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:
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, 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):
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()]

559
Lib/fontTools/subset/cff.py Normal file
View File

@ -0,0 +1,559 @@
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
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 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:
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, 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):
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, 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 charString.isCFF2:
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, 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
del 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('CFF2'))
def desubroutinize(self, font):
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']

View File

@ -6,17 +6,21 @@ $ 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, 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
@ -31,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.
@ -59,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")
@ -99,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(varfont)
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
@ -168,15 +299,15 @@ def instantiateVariableFont(varfont, location, inplace=False):
(gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
del varfont['GDEF']
addidef = False
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 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 = []

View File

@ -162,19 +162,27 @@ class VarStoreInstancer(object):
self._scalars[regionIdx] = scalar
return scalar
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]
@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]
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)
#
# Optimizations

Binary file not shown.

View File

@ -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>

View File

@ -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
@ -160,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())