Merge remote-tracking branch 'origin/master' into partial-instancer
This commit is contained in:
commit
f74e82fec5
@ -5,6 +5,6 @@ from fontTools.misc.loggingTools import configLogger
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
version = __version__ = "3.40.1.dev0"
|
version = __version__ = "3.41.1.dev0"
|
||||||
|
|
||||||
__all__ = ["version", "log", "configLogger"]
|
__all__ = ["version", "log", "configLogger"]
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
from __future__ import print_function, division, absolute_import
|
from __future__ import print_function, division, absolute_import
|
||||||
from fontTools.misc.py23 import *
|
from fontTools.misc.py23 import *
|
||||||
|
from fontTools.cffLib import maxStackLimit
|
||||||
|
|
||||||
|
|
||||||
def stringToProgram(string):
|
def stringToProgram(string):
|
||||||
@ -26,14 +27,21 @@ def programToString(program):
|
|||||||
return ' '.join(str(x) for x in program)
|
return ' '.join(str(x) for x in program)
|
||||||
|
|
||||||
|
|
||||||
def programToCommands(program):
|
def programToCommands(program, getNumRegions=None):
|
||||||
r"""Takes a T2CharString program list and returns list of commands.
|
"""Takes a T2CharString program list and returns list of commands.
|
||||||
Each command is a two-tuple of commandname,arg-list. The commandname might
|
Each command is a two-tuple of commandname,arg-list. The commandname might
|
||||||
be empty string if no commandname shall be emitted (used for glyph width,
|
be empty string if no commandname shall be emitted (used for glyph width,
|
||||||
hintmask/cntrmask argument, as well as stray arguments at the end of the
|
hintmask/cntrmask argument, as well as stray arguments at the end of the
|
||||||
program (¯\_(ツ)_/¯)."""
|
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."""
|
||||||
|
|
||||||
width = None
|
width = None
|
||||||
|
seenWidthOp = False
|
||||||
|
vsIndex = None
|
||||||
commands = []
|
commands = []
|
||||||
stack = []
|
stack = []
|
||||||
it = iter(program)
|
it = iter(program)
|
||||||
@ -42,10 +50,37 @@ def programToCommands(program):
|
|||||||
stack.append(token)
|
stack.append(token)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if width is None and token in {'hstem', 'hstemhm', 'vstem', 'vstemhm',
|
if token == 'blend':
|
||||||
'cntrmask', 'hintmask',
|
assert getNumRegions is not None
|
||||||
'hmoveto', 'vmoveto', 'rmoveto',
|
numSourceFonts = 1 + getNumRegions(vsIndex)
|
||||||
'endchar'}:
|
# replace the blend op args on the stack with a single list
|
||||||
|
# containing all the blend op args.
|
||||||
|
numBlendOps = stack[-1] * numSourceFonts + 1
|
||||||
|
# replace first blend op by a list of the blend ops.
|
||||||
|
stack[-numBlendOps:] = [stack[-numBlendOps:]]
|
||||||
|
|
||||||
|
# Check for width.
|
||||||
|
if not seenWidthOp:
|
||||||
|
seenWidthOp = True
|
||||||
|
widthLen = len(stack) - numBlendOps
|
||||||
|
if widthLen and (widthLen % 2):
|
||||||
|
stack.pop(0)
|
||||||
|
elif width is not None:
|
||||||
|
commands.pop(0)
|
||||||
|
width = None
|
||||||
|
# We do NOT add the width to the command list if a blend is seen:
|
||||||
|
# if a blend op exists, this is or will be a CFF2 charstring.
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif token == 'vsindex':
|
||||||
|
vsIndex = stack[-1]
|
||||||
|
assert type(vsIndex) is int
|
||||||
|
|
||||||
|
elif (not seenWidthOp) and token in {'hstem', 'hstemhm', 'vstem', 'vstemhm',
|
||||||
|
'cntrmask', 'hintmask',
|
||||||
|
'hmoveto', 'vmoveto', 'rmoveto',
|
||||||
|
'endchar'}:
|
||||||
|
seenWidthOp = True
|
||||||
parity = token in {'hmoveto', 'vmoveto'}
|
parity = token in {'hmoveto', 'vmoveto'}
|
||||||
if stack and (len(stack) % 2) ^ parity:
|
if stack and (len(stack) % 2) ^ parity:
|
||||||
width = stack.pop(0)
|
width = stack.pop(0)
|
||||||
@ -64,11 +99,23 @@ def programToCommands(program):
|
|||||||
return commands
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
def _flattenBlendArgs(args):
|
||||||
|
token_list = []
|
||||||
|
for arg in args:
|
||||||
|
if isinstance(arg, list):
|
||||||
|
token_list.extend(arg)
|
||||||
|
token_list.append('blend')
|
||||||
|
else:
|
||||||
|
token_list.append(arg)
|
||||||
|
return token_list
|
||||||
|
|
||||||
def commandsToProgram(commands):
|
def commandsToProgram(commands):
|
||||||
"""Takes a commands list as returned by programToCommands() and converts
|
"""Takes a commands list as returned by programToCommands() and converts
|
||||||
it back to a T2CharString program list."""
|
it back to a T2CharString program list."""
|
||||||
program = []
|
program = []
|
||||||
for op,args in commands:
|
for op,args in commands:
|
||||||
|
if any(isinstance(arg, list) for arg in args):
|
||||||
|
args = _flattenBlendArgs(args)
|
||||||
program.extend(args)
|
program.extend(args)
|
||||||
if op:
|
if op:
|
||||||
program.append(op)
|
program.append(op)
|
||||||
@ -203,11 +250,58 @@ class _GeneralizerDecombinerCommandsMap(object):
|
|||||||
yield ('rlineto', args)
|
yield ('rlineto', args)
|
||||||
yield ('rrcurveto', last_args)
|
yield ('rrcurveto', last_args)
|
||||||
|
|
||||||
|
def _convertBlendOpToArgs(blendList):
|
||||||
|
# args is list of blend op args. Since we are supporting
|
||||||
|
# recursive blend op calls, some of these args may also
|
||||||
|
# be a list of blend op args, and need to be converted before
|
||||||
|
# we convert the current list.
|
||||||
|
if any([isinstance(arg, list) for arg in blendList]):
|
||||||
|
args = [i for e in blendList for i in
|
||||||
|
(_convertBlendOpToArgs(e) if isinstance(e,list) else [e]) ]
|
||||||
|
else:
|
||||||
|
args = blendList
|
||||||
|
|
||||||
|
# We now know that blendList contains a blend op argument list, even if
|
||||||
|
# some of the args are lists that each contain a blend op argument list.
|
||||||
|
# Convert from:
|
||||||
|
# [default font arg sequence x0,...,xn] + [delta tuple for x0] + ... + [delta tuple for xn]
|
||||||
|
# to:
|
||||||
|
# [ [x0] + [delta tuple for x0],
|
||||||
|
# ...,
|
||||||
|
# [xn] + [delta tuple for xn] ]
|
||||||
|
numBlends = args[-1]
|
||||||
|
# Can't use args.pop() when the args are being used in a nested list
|
||||||
|
# comprehension. See calling context
|
||||||
|
args = args[:-1]
|
||||||
|
|
||||||
|
numRegions = len(args)//numBlends - 1
|
||||||
|
if not (numBlends*(numRegions + 1) == len(args)):
|
||||||
|
raise ValueError(blendList)
|
||||||
|
|
||||||
|
defaultArgs = [[arg] for arg in args[:numBlends]]
|
||||||
|
deltaArgs = args[numBlends:]
|
||||||
|
numDeltaValues = len(deltaArgs)
|
||||||
|
deltaList = [ deltaArgs[i:i + numRegions] for i in range(0, numDeltaValues, numRegions) ]
|
||||||
|
blend_args = [ a + b for a, b in zip(defaultArgs,deltaList)]
|
||||||
|
return blend_args
|
||||||
|
|
||||||
def generalizeCommands(commands, ignoreErrors=False):
|
def generalizeCommands(commands, ignoreErrors=False):
|
||||||
result = []
|
result = []
|
||||||
mapping = _GeneralizerDecombinerCommandsMap
|
mapping = _GeneralizerDecombinerCommandsMap
|
||||||
for op,args in commands:
|
for op, args in commands:
|
||||||
|
# First, generalize any blend args in the arg list.
|
||||||
|
if any([isinstance(arg, list) for arg in args]):
|
||||||
|
try:
|
||||||
|
args = [n for arg in args for n in (_convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg])]
|
||||||
|
except ValueError:
|
||||||
|
if ignoreErrors:
|
||||||
|
# Store op as data, such that consumers of commands do not have to
|
||||||
|
# deal with incorrect number of arguments.
|
||||||
|
result.append(('', args))
|
||||||
|
result.append(('', [op]))
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
func = getattr(mapping, op, None)
|
func = getattr(mapping, op, None)
|
||||||
if not func:
|
if not func:
|
||||||
result.append((op,args))
|
result.append((op,args))
|
||||||
@ -225,8 +319,8 @@ def generalizeCommands(commands, ignoreErrors=False):
|
|||||||
raise
|
raise
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def generalizeProgram(program, **kwargs):
|
def generalizeProgram(program, getNumRegions=None, **kwargs):
|
||||||
return commandsToProgram(generalizeCommands(programToCommands(program), **kwargs))
|
return commandsToProgram(generalizeCommands(programToCommands(program, getNumRegions), **kwargs))
|
||||||
|
|
||||||
|
|
||||||
def _categorizeVector(v):
|
def _categorizeVector(v):
|
||||||
@ -267,6 +361,70 @@ def _negateCategory(a):
|
|||||||
assert a in '0r'
|
assert a in '0r'
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
def _convertToBlendCmds(args):
|
||||||
|
# return a list of blend commands, and
|
||||||
|
# the remaining non-blended args, if any.
|
||||||
|
num_args = len(args)
|
||||||
|
stack_use = 0
|
||||||
|
new_args = []
|
||||||
|
i = 0
|
||||||
|
while i < num_args:
|
||||||
|
arg = args[i]
|
||||||
|
if not isinstance(arg, list):
|
||||||
|
new_args.append(arg)
|
||||||
|
i += 1
|
||||||
|
stack_use += 1
|
||||||
|
else:
|
||||||
|
prev_stack_use = stack_use
|
||||||
|
# The arg is a tuple of blend values.
|
||||||
|
# These are each (master 0,delta 1..delta n)
|
||||||
|
# Combine as many successive tuples as we can,
|
||||||
|
# up to the max stack limit.
|
||||||
|
num_sources = len(arg)
|
||||||
|
blendlist = [arg]
|
||||||
|
i += 1
|
||||||
|
stack_use += 1 + num_sources # 1 for the num_blends arg
|
||||||
|
while (i < num_args) and isinstance(args[i], list):
|
||||||
|
blendlist.append(args[i])
|
||||||
|
i += 1
|
||||||
|
stack_use += num_sources
|
||||||
|
if stack_use + num_sources > maxStackLimit:
|
||||||
|
# if we are here, max stack is the CFF2 max stack.
|
||||||
|
# I use the CFF2 max stack limit here rather than
|
||||||
|
# the 'maxstack' chosen by the client, as the default
|
||||||
|
# maxstack may have been used unintentionally. For all
|
||||||
|
# the other operators, this just produces a little less
|
||||||
|
# optimization, but here it puts a hard (and low) limit
|
||||||
|
# on the number of source fonts that can be used.
|
||||||
|
break
|
||||||
|
# blendList now contains as many single blend tuples as can be
|
||||||
|
# combined without exceeding the CFF2 stack limit.
|
||||||
|
num_blends = len(blendlist)
|
||||||
|
# append the 'num_blends' default font values
|
||||||
|
blend_args = []
|
||||||
|
for arg in blendlist:
|
||||||
|
blend_args.append(arg[0])
|
||||||
|
for arg in blendlist:
|
||||||
|
blend_args.extend(arg[1:])
|
||||||
|
blend_args.append(num_blends)
|
||||||
|
new_args.append(blend_args)
|
||||||
|
stack_use = prev_stack_use + num_blends
|
||||||
|
|
||||||
|
return new_args
|
||||||
|
|
||||||
|
def _addArgs(a, b):
|
||||||
|
if isinstance(b, list):
|
||||||
|
if isinstance(a, list):
|
||||||
|
if len(a) != len(b):
|
||||||
|
raise ValueError()
|
||||||
|
return [_addArgs(va, vb) for va,vb in zip(a, b)]
|
||||||
|
else:
|
||||||
|
a, b = b, a
|
||||||
|
if isinstance(a, list):
|
||||||
|
return [_addArgs(a[0], b)] + a[1:]
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
|
||||||
def specializeCommands(commands,
|
def specializeCommands(commands,
|
||||||
ignoreErrors=False,
|
ignoreErrors=False,
|
||||||
generalizeFirst=True,
|
generalizeFirst=True,
|
||||||
@ -302,6 +460,8 @@ def specializeCommands(commands,
|
|||||||
# I have convinced myself that this produces optimal bytecode (except for, possibly
|
# I have convinced myself that this produces optimal bytecode (except for, possibly
|
||||||
# one byte each time maxstack size prohibits combining.) YMMV, but you'd be wrong. :-)
|
# one byte each time maxstack size prohibits combining.) YMMV, but you'd be wrong. :-)
|
||||||
# A dynamic-programming approach can do the same but would be significantly slower.
|
# A dynamic-programming approach can do the same but would be significantly slower.
|
||||||
|
#
|
||||||
|
# 7. For any args which are blend lists, convert them to a blend command.
|
||||||
|
|
||||||
|
|
||||||
# 0. Generalize commands.
|
# 0. Generalize commands.
|
||||||
@ -417,12 +577,18 @@ def specializeCommands(commands,
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Merge adjacent hlineto's and vlineto's.
|
# Merge adjacent hlineto's and vlineto's.
|
||||||
|
# In CFF2 charstrings from variable fonts, each
|
||||||
|
# arg item may be a list of blendable values, one from
|
||||||
|
# each source font.
|
||||||
if (i and op in {'hlineto', 'vlineto'} and
|
if (i and op in {'hlineto', 'vlineto'} and
|
||||||
(op == commands[i-1][0]) and
|
(op == commands[i-1][0])):
|
||||||
(not isinstance(args[0], list))):
|
|
||||||
_, other_args = commands[i-1]
|
_, other_args = commands[i-1]
|
||||||
assert len(args) == 1 and len(other_args) == 1
|
assert len(args) == 1 and len(other_args) == 1
|
||||||
commands[i-1] = (op, [other_args[0]+args[0]])
|
try:
|
||||||
|
new_args = [_addArgs(args[0], other_args[0])]
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
commands[i-1] = (op, new_args)
|
||||||
del commands[i]
|
del commands[i]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -534,10 +700,16 @@ def specializeCommands(commands,
|
|||||||
commands[i] = op0+op1+'curveto', args
|
commands[i] = op0+op1+'curveto', args
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 7. For any series of args which are blend lists, convert the series to a single blend arg.
|
||||||
|
for i in range(len(commands)):
|
||||||
|
op, args = commands[i]
|
||||||
|
if any(isinstance(arg, list) for arg in args):
|
||||||
|
commands[i] = op, _convertToBlendCmds(args)
|
||||||
|
|
||||||
return commands
|
return commands
|
||||||
|
|
||||||
def specializeProgram(program, **kwargs):
|
def specializeProgram(program, getNumRegions=None, **kwargs):
|
||||||
return commandsToProgram(specializeCommands(programToCommands(program), **kwargs))
|
return commandsToProgram(specializeCommands(programToCommands(program, getNumRegions), **kwargs))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@ -554,4 +726,3 @@ if __name__ == '__main__':
|
|||||||
assert program == program2
|
assert program == program2
|
||||||
print("Generalized program:"); print(programToString(generalizeProgram(program)))
|
print("Generalized program:"); print(programToString(generalizeProgram(program)))
|
||||||
print("Specialized program:"); print(programToString(specializeProgram(program)))
|
print("Specialized program:"); print(programToString(specializeProgram(program)))
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from fontTools.feaLib.error import FeatureLibError
|
|||||||
from fontTools.feaLib.parser import Parser
|
from fontTools.feaLib.parser import Parser
|
||||||
from fontTools.feaLib.ast import FeatureFile
|
from fontTools.feaLib.ast import FeatureFile
|
||||||
from fontTools.otlLib import builder as otl
|
from fontTools.otlLib import builder as otl
|
||||||
|
from fontTools.otlLib.maxContextCalc import maxCtxFont
|
||||||
from fontTools.ttLib import newTable, getTableModule
|
from fontTools.ttLib import newTable, getTableModule
|
||||||
from fontTools.ttLib.tables import otBase, otTables
|
from fontTools.ttLib.tables import otBase, otTables
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import defaultdict, OrderedDict
|
||||||
@ -137,6 +138,9 @@ class Builder(object):
|
|||||||
fontTable.table = table
|
fontTable.table = table
|
||||||
elif tag in self.font:
|
elif tag in self.font:
|
||||||
del self.font[tag]
|
del self.font[tag]
|
||||||
|
if (any(tag in self.font for tag in ("GPOS", "GSUB")) and
|
||||||
|
"OS/2" in self.font):
|
||||||
|
self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
|
||||||
if "GDEF" in tables:
|
if "GDEF" in tables:
|
||||||
gdef = self.buildGDEF()
|
gdef = self.buildGDEF()
|
||||||
if gdef:
|
if gdef:
|
||||||
|
@ -21,6 +21,7 @@ that works:
|
|||||||
fb.setupHorizontalHeader()
|
fb.setupHorizontalHeader()
|
||||||
fb.setupNameTable(...)
|
fb.setupNameTable(...)
|
||||||
fb.setupOS2()
|
fb.setupOS2()
|
||||||
|
fb.addOpenTypeFeatures(...)
|
||||||
fb.setupPost()
|
fb.setupPost()
|
||||||
fb.save(...)
|
fb.save(...)
|
||||||
|
|
||||||
@ -299,7 +300,7 @@ _OS2Defaults = dict(
|
|||||||
sCapHeight = 0,
|
sCapHeight = 0,
|
||||||
usDefaultChar = 0, # .notdef
|
usDefaultChar = 0, # .notdef
|
||||||
usBreakChar = 32, # space
|
usBreakChar = 32, # space
|
||||||
usMaxContext = 2, # just kerning
|
usMaxContext = 0,
|
||||||
usLowerOpticalPointSize = 0,
|
usLowerOpticalPointSize = 0,
|
||||||
usUpperOpticalPointSize = 0,
|
usUpperOpticalPointSize = 0,
|
||||||
)
|
)
|
||||||
|
@ -944,6 +944,16 @@ class T2CharString(object):
|
|||||||
self.program = program
|
self.program = program
|
||||||
self.private = private
|
self.private = private
|
||||||
self.globalSubrs = globalSubrs if globalSubrs is not None else []
|
self.globalSubrs = globalSubrs if globalSubrs is not None else []
|
||||||
|
self._cur_vsindex = None
|
||||||
|
|
||||||
|
def getNumRegions(self, vsindex=None):
|
||||||
|
pd = self.private
|
||||||
|
assert(pd is not None)
|
||||||
|
if vsindex is not None:
|
||||||
|
self._cur_vsindex = vsindex
|
||||||
|
elif self._cur_vsindex is None:
|
||||||
|
self._cur_vsindex = pd.vsindex if hasattr(pd, 'vsindex') else 0
|
||||||
|
return pd.getNumRegions(self._cur_vsindex)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.bytecode is None:
|
if self.bytecode is None:
|
||||||
|
101
Lib/fontTools/otlLib/maxContextCalc.py
Normal file
101
Lib/fontTools/otlLib/maxContextCalc.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import, unicode_literals
|
||||||
|
|
||||||
|
__all__ = ['maxCtxFont']
|
||||||
|
|
||||||
|
|
||||||
|
def maxCtxFont(font):
|
||||||
|
"""Calculate the usMaxContext value for an entire font."""
|
||||||
|
|
||||||
|
maxCtx = 0
|
||||||
|
for tag in ('GSUB', 'GPOS'):
|
||||||
|
if tag not in font:
|
||||||
|
continue
|
||||||
|
table = font[tag].table
|
||||||
|
if not table.LookupList:
|
||||||
|
continue
|
||||||
|
for lookup in table.LookupList.Lookup:
|
||||||
|
for st in lookup.SubTable:
|
||||||
|
maxCtx = maxCtxSubtable(maxCtx, tag, lookup.LookupType, st)
|
||||||
|
return maxCtx
|
||||||
|
|
||||||
|
|
||||||
|
def maxCtxSubtable(maxCtx, tag, lookupType, st):
|
||||||
|
"""Calculate usMaxContext based on a single lookup table (and an existing
|
||||||
|
max value).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# single positioning, single / multiple substitution
|
||||||
|
if (tag == 'GPOS' and lookupType == 1) or (
|
||||||
|
tag == 'GSUB' and lookupType in (1, 2, 3)):
|
||||||
|
maxCtx = max(maxCtx, 1)
|
||||||
|
|
||||||
|
# pair positioning
|
||||||
|
elif tag == 'GPOS' and lookupType == 2:
|
||||||
|
maxCtx = max(maxCtx, 2)
|
||||||
|
|
||||||
|
# ligatures
|
||||||
|
elif tag == 'GSUB' and lookupType == 4:
|
||||||
|
for ligatures in st.ligatures.values():
|
||||||
|
for ligature in ligatures:
|
||||||
|
maxCtx = max(maxCtx, ligature.CompCount)
|
||||||
|
|
||||||
|
# context
|
||||||
|
elif (tag == 'GPOS' and lookupType == 7) or (
|
||||||
|
tag == 'GSUB' and lookupType == 5):
|
||||||
|
maxCtx = maxCtxContextualSubtable(
|
||||||
|
maxCtx, st, 'Pos' if tag == 'GPOS' else 'Sub')
|
||||||
|
|
||||||
|
# chained context
|
||||||
|
elif (tag == 'GPOS' and lookupType == 8) or (
|
||||||
|
tag == 'GSUB' and lookupType == 6):
|
||||||
|
maxCtx = maxCtxContextualSubtable(
|
||||||
|
maxCtx, st, 'Pos' if tag == 'GPOS' else 'Sub', 'Chain')
|
||||||
|
|
||||||
|
# extensions
|
||||||
|
elif (tag == 'GPOS' and lookupType == 9) or (
|
||||||
|
tag == 'GSUB' and lookupType == 7):
|
||||||
|
maxCtx = maxCtxSubtable(
|
||||||
|
maxCtx, tag, st.ExtensionLookupType, st.ExtSubTable)
|
||||||
|
|
||||||
|
# reverse-chained context
|
||||||
|
elif tag == 'GSUB' and lookupType == 8:
|
||||||
|
maxCtx = maxCtxContextualRule(maxCtx, st, 'Reverse')
|
||||||
|
|
||||||
|
return maxCtx
|
||||||
|
|
||||||
|
|
||||||
|
def maxCtxContextualSubtable(maxCtx, st, ruleType, chain=''):
|
||||||
|
"""Calculate usMaxContext based on a contextual feature subtable."""
|
||||||
|
|
||||||
|
if st.Format == 1:
|
||||||
|
for ruleset in getattr(st, '%s%sRuleSet' % (chain, ruleType)):
|
||||||
|
if ruleset is None:
|
||||||
|
continue
|
||||||
|
for rule in getattr(ruleset, '%s%sRule' % (chain, ruleType)):
|
||||||
|
if rule is None:
|
||||||
|
continue
|
||||||
|
maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
|
||||||
|
|
||||||
|
elif st.Format == 2:
|
||||||
|
for ruleset in getattr(st, '%s%sClassSet' % (chain, ruleType)):
|
||||||
|
if ruleset is None:
|
||||||
|
continue
|
||||||
|
for rule in getattr(ruleset, '%s%sClassRule' % (chain, ruleType)):
|
||||||
|
if rule is None:
|
||||||
|
continue
|
||||||
|
maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
|
||||||
|
|
||||||
|
elif st.Format == 3:
|
||||||
|
maxCtx = maxCtxContextualRule(maxCtx, st, chain)
|
||||||
|
|
||||||
|
return maxCtx
|
||||||
|
|
||||||
|
|
||||||
|
def maxCtxContextualRule(maxCtx, st, chain):
|
||||||
|
"""Calculate usMaxContext based on a contextual feature rule."""
|
||||||
|
|
||||||
|
if not chain:
|
||||||
|
return max(maxCtx, st.GlyphCount)
|
||||||
|
elif chain == 'Reverse':
|
||||||
|
return max(maxCtx, st.GlyphCount + st.LookAheadGlyphCount)
|
||||||
|
return max(maxCtx, st.InputGlyphCount + st.LookAheadGlyphCount)
|
@ -7,6 +7,7 @@ from fontTools.misc.py23 import *
|
|||||||
from fontTools.misc.fixedTools import otRound
|
from fontTools.misc.fixedTools import otRound
|
||||||
from fontTools import ttLib
|
from fontTools import ttLib
|
||||||
from fontTools.ttLib.tables import otTables
|
from fontTools.ttLib.tables import otTables
|
||||||
|
from fontTools.otlLib.maxContextCalc import maxCtxFont
|
||||||
from fontTools.pens.basePen import NullPen
|
from fontTools.pens.basePen import NullPen
|
||||||
from fontTools.misc.loggingTools import Timer
|
from fontTools.misc.loggingTools import Timer
|
||||||
from fontTools.subset.cff import *
|
from fontTools.subset.cff import *
|
||||||
@ -322,6 +323,10 @@ Other font-specific options:
|
|||||||
Update the 'OS/2 xAvgCharWidth' field after subsetting.
|
Update the 'OS/2 xAvgCharWidth' field after subsetting.
|
||||||
--no-recalc-average-width
|
--no-recalc-average-width
|
||||||
Don't change the 'OS/2 xAvgCharWidth' field. [default]
|
Don't change the 'OS/2 xAvgCharWidth' field. [default]
|
||||||
|
--recalc-max-context
|
||||||
|
Update the 'OS/2 usMaxContext' field after subsetting.
|
||||||
|
--no-recalc-max-context
|
||||||
|
Don't change the 'OS/2 usMaxContext' field. [default]
|
||||||
--font-number=<number>
|
--font-number=<number>
|
||||||
Select font number for TrueType Collection (.ttc/.otc), starting from 0.
|
Select font number for TrueType Collection (.ttc/.otc), starting from 0.
|
||||||
|
|
||||||
@ -1789,39 +1794,47 @@ def subset_glyphs(self, s):
|
|||||||
self.glyphCount = len(self.variations)
|
self.glyphCount = len(self.variations)
|
||||||
return bool(self.variations)
|
return bool(self.variations)
|
||||||
|
|
||||||
|
def _remap_index_map(s, varidx_map, table_map):
|
||||||
|
map_ = {k:varidx_map[v] for k,v in table_map.mapping.items()}
|
||||||
|
# Emptied glyphs are remapped to:
|
||||||
|
# if GID <= last retained GID, 0/0: delta set for 0/0 is expected to exist & zeros compress well
|
||||||
|
# if GID > last retained GID, major/minor of the last retained glyph: will be optimized out by table compiler
|
||||||
|
last_idx = varidx_map[table_map.mapping[s.last_retained_glyph]]
|
||||||
|
for g,i in s.reverseEmptiedGlyphMap.items():
|
||||||
|
map_[g] = last_idx if i > s.last_retained_order else 0
|
||||||
|
return map_
|
||||||
|
|
||||||
@_add_method(ttLib.getTableClass('HVAR'))
|
@_add_method(ttLib.getTableClass('HVAR'))
|
||||||
def subset_glyphs(self, s):
|
def subset_glyphs(self, s):
|
||||||
table = self.table
|
table = self.table
|
||||||
|
|
||||||
# TODO Update for retain_gids
|
|
||||||
|
|
||||||
used = set()
|
used = set()
|
||||||
|
advIdxes_ = set()
|
||||||
|
retainAdvMap = False
|
||||||
|
|
||||||
if table.AdvWidthMap:
|
if table.AdvWidthMap:
|
||||||
if not s.options.retain_gids:
|
table.AdvWidthMap.mapping = _dict_subset(table.AdvWidthMap.mapping, s.glyphs)
|
||||||
table.AdvWidthMap.mapping = _dict_subset(table.AdvWidthMap.mapping, s.glyphs)
|
|
||||||
used.update(table.AdvWidthMap.mapping.values())
|
used.update(table.AdvWidthMap.mapping.values())
|
||||||
else:
|
else:
|
||||||
assert table.LsbMap is None and table.RsbMap is None, "File a bug."
|
|
||||||
used.update(s.reverseOrigGlyphMap.values())
|
used.update(s.reverseOrigGlyphMap.values())
|
||||||
|
advIdxes_ = used.copy()
|
||||||
|
retainAdvMap = s.options.retain_gids
|
||||||
|
|
||||||
if table.LsbMap:
|
if table.LsbMap:
|
||||||
if not s.options.retain_gids:
|
table.LsbMap.mapping = _dict_subset(table.LsbMap.mapping, s.glyphs)
|
||||||
table.LsbMap.mapping = _dict_subset(table.LsbMap.mapping, s.glyphs)
|
|
||||||
used.update(table.LsbMap.mapping.values())
|
used.update(table.LsbMap.mapping.values())
|
||||||
if table.RsbMap:
|
if table.RsbMap:
|
||||||
if not s.options.retain_gids:
|
table.RsbMap.mapping = _dict_subset(table.RsbMap.mapping, s.glyphs)
|
||||||
table.RsbMap.mapping = _dict_subset(table.RsbMap.mapping, s.glyphs)
|
|
||||||
used.update(table.RsbMap.mapping.values())
|
used.update(table.RsbMap.mapping.values())
|
||||||
|
|
||||||
varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used)
|
varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_)
|
||||||
|
|
||||||
if table.AdvWidthMap:
|
if table.AdvWidthMap:
|
||||||
table.AdvWidthMap.mapping = {k:varidx_map[v] for k,v in table.AdvWidthMap.mapping.items()}
|
table.AdvWidthMap.mapping = _remap_index_map(s, varidx_map, table.AdvWidthMap)
|
||||||
if table.LsbMap:
|
if table.LsbMap:
|
||||||
table.LsbMap.mapping = {k:varidx_map[v] for k,v in table.LsbMap.mapping.items()}
|
table.LsbMap.mapping = _remap_index_map(s, varidx_map, table.LsbMap)
|
||||||
if table.RsbMap:
|
if table.RsbMap:
|
||||||
table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()}
|
table.RsbMap.mapping = _remap_index_map(s, varidx_map, table.RsbMap)
|
||||||
|
|
||||||
# TODO Return emptiness...
|
# TODO Return emptiness...
|
||||||
return True
|
return True
|
||||||
@ -1831,37 +1844,37 @@ def subset_glyphs(self, s):
|
|||||||
table = self.table
|
table = self.table
|
||||||
|
|
||||||
used = set()
|
used = set()
|
||||||
|
advIdxes_ = set()
|
||||||
|
retainAdvMap = False
|
||||||
|
|
||||||
if table.AdvHeightMap:
|
if table.AdvHeightMap:
|
||||||
if not s.options.retain_gids:
|
table.AdvHeightMap.mapping = _dict_subset(table.AdvHeightMap.mapping, s.glyphs)
|
||||||
table.AdvHeightMap.mapping = _dict_subset(table.AdvHeightMap.mapping, s.glyphs)
|
|
||||||
used.update(table.AdvHeightMap.mapping.values())
|
used.update(table.AdvHeightMap.mapping.values())
|
||||||
else:
|
else:
|
||||||
assert table.TsbMap is None and table.BsbMap is None and table.VOrgMap is None, "File a bug."
|
|
||||||
used.update(s.reverseOrigGlyphMap.values())
|
used.update(s.reverseOrigGlyphMap.values())
|
||||||
|
advIdxes_ = used.copy()
|
||||||
|
retainAdvMap = s.options.retain_gids
|
||||||
|
|
||||||
if table.TsbMap:
|
if table.TsbMap:
|
||||||
if not s.options.retain_gids:
|
table.TsbMap.mapping = _dict_subset(table.TsbMap.mapping, s.glyphs)
|
||||||
table.TsbMap.mapping = _dict_subset(table.TsbMap.mapping, s.glyphs)
|
|
||||||
used.update(table.TsbMap.mapping.values())
|
used.update(table.TsbMap.mapping.values())
|
||||||
if table.BsbMap:
|
if table.BsbMap:
|
||||||
if not s.options.retain_gids:
|
table.BsbMap.mapping = _dict_subset(table.BsbMap.mapping, s.glyphs)
|
||||||
table.BsbMap.mapping = _dict_subset(table.BsbMap.mapping, s.glyphs)
|
|
||||||
used.update(table.BsbMap.mapping.values())
|
used.update(table.BsbMap.mapping.values())
|
||||||
if table.VOrgMap:
|
if table.VOrgMap:
|
||||||
if not s.options.retain_gids:
|
table.VOrgMap.mapping = _dict_subset(table.VOrgMap.mapping, s.glyphs)
|
||||||
table.VOrgMap.mapping = _dict_subset(table.VOrgMap.mapping, s.glyphs)
|
|
||||||
used.update(table.VOrgMap.mapping.values())
|
used.update(table.VOrgMap.mapping.values())
|
||||||
|
|
||||||
varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used)
|
varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_)
|
||||||
|
|
||||||
if table.AdvHeightMap:
|
if table.AdvHeightMap:
|
||||||
table.AdvHeightMap.mapping = {k:varidx_map[v] for k,v in table.AdvHeightMap.mapping.items()}
|
table.AdvHeightMap.mapping = _remap_index_map(s, varidx_map, table.AdvHeightMap)
|
||||||
if table.TsbMap:
|
if table.TsbMap:
|
||||||
table.TsbMap.mapping = {k:varidx_map[v] for k,v in table.TsbMap.mapping.items()}
|
table.TsbMap.mapping = _remap_index_map(s, varidx_map, table.TsbMap)
|
||||||
if table.BsbMap:
|
if table.BsbMap:
|
||||||
table.BsbMap.mapping = {k:varidx_map[v] for k,v in table.BsbMap.mapping.items()}
|
table.BsbMap.mapping = _remap_index_map(s, varidx_map, table.BsbMap)
|
||||||
if table.VOrgMap:
|
if table.VOrgMap:
|
||||||
table.VOrgMap.mapping = {k:varidx_map[v] for k,v in table.VOrgMap.mapping.items()}
|
table.VOrgMap.mapping = _remap_index_map(s, varidx_map, table.VOrgMap)
|
||||||
|
|
||||||
# TODO Return emptiness...
|
# TODO Return emptiness...
|
||||||
return True
|
return True
|
||||||
@ -2305,6 +2318,7 @@ class Options(object):
|
|||||||
self.recalc_timestamp = False # Recalculate font modified timestamp
|
self.recalc_timestamp = False # Recalculate font modified timestamp
|
||||||
self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits
|
self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits
|
||||||
self.recalc_average_width = False # update 'xAvgCharWidth'
|
self.recalc_average_width = False # update 'xAvgCharWidth'
|
||||||
|
self.recalc_max_context = False # update 'usMaxContext'
|
||||||
self.canonical_order = None # Order tables as recommended
|
self.canonical_order = None # Order tables as recommended
|
||||||
self.flavor = None # May be 'woff' or 'woff2'
|
self.flavor = None # May be 'woff' or 'woff2'
|
||||||
self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0
|
self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0
|
||||||
@ -2565,6 +2579,9 @@ class Subsetter(object):
|
|||||||
|
|
||||||
order = font.getReverseGlyphMap()
|
order = font.getReverseGlyphMap()
|
||||||
self.reverseOrigGlyphMap = {g:order[g] for g in self.glyphs_retained}
|
self.reverseOrigGlyphMap = {g:order[g] for g in self.glyphs_retained}
|
||||||
|
self.reverseEmptiedGlyphMap = {g:order[g] for g in self.glyphs_emptied}
|
||||||
|
self.last_retained_order = max(self.reverseOrigGlyphMap.values())
|
||||||
|
self.last_retained_glyph = font.getGlyphOrder()[self.last_retained_order]
|
||||||
|
|
||||||
log.info("Retaining %d glyphs", len(self.glyphs_retained))
|
log.info("Retaining %d glyphs", len(self.glyphs_retained))
|
||||||
|
|
||||||
@ -2614,6 +2631,11 @@ class Subsetter(object):
|
|||||||
if avg_width != font[tag].xAvgCharWidth:
|
if avg_width != font[tag].xAvgCharWidth:
|
||||||
font[tag].xAvgCharWidth = avg_width
|
font[tag].xAvgCharWidth = avg_width
|
||||||
log.info("%s xAvgCharWidth updated: %d", tag, avg_width)
|
log.info("%s xAvgCharWidth updated: %d", tag, avg_width)
|
||||||
|
if self.options.recalc_max_context:
|
||||||
|
max_context = maxCtxFont(font)
|
||||||
|
if max_context != font[tag].usMaxContext:
|
||||||
|
font[tag].usMaxContext = max_context
|
||||||
|
log.info("%s usMaxContext updated: %d", tag, max_context)
|
||||||
clazz = ttLib.getTableClass(tag)
|
clazz = ttLib.getTableClass(tag)
|
||||||
if hasattr(clazz, 'prune_post_subset'):
|
if hasattr(clazz, 'prune_post_subset'):
|
||||||
with timer("prune '%s'" % tag):
|
with timer("prune '%s'" % tag):
|
||||||
|
@ -406,7 +406,7 @@ class Silf(object):
|
|||||||
if version >= 3.0:
|
if version >= 3.0:
|
||||||
pseudo = sstruct.unpack(Silf_pseudomap_format, data[8+6*i:14+6*i], _Object())
|
pseudo = sstruct.unpack(Silf_pseudomap_format, data[8+6*i:14+6*i], _Object())
|
||||||
else:
|
else:
|
||||||
pseudo = struct.unpack('>HH', data[8+4*i:12+4*i], _Object())
|
pseudo = sstruct.unpack('>HH', data[8+4*i:12+4*i], _Object())
|
||||||
self.pMap[pseudo.unicode] = ttFont.getGlyphName(pseudo.nPseudo)
|
self.pMap[pseudo.unicode] = ttFont.getGlyphName(pseudo.nPseudo)
|
||||||
data = data[8 + 6 * numPseudo:]
|
data = data[8 + 6 * numPseudo:]
|
||||||
currpos = (sstruct.calcsize(Silf_part1_format)
|
currpos = (sstruct.calcsize(Silf_part1_format)
|
||||||
|
@ -669,12 +669,11 @@ _DesignSpaceData = namedtuple(
|
|||||||
|
|
||||||
|
|
||||||
def _add_CFF2(varFont, model, master_fonts):
|
def _add_CFF2(varFont, model, master_fonts):
|
||||||
from .cff import (convertCFFtoCFF2, addCFFVarStore, merge_region_fonts)
|
from .cff import (convertCFFtoCFF2, merge_region_fonts)
|
||||||
glyphOrder = varFont.getGlyphOrder()
|
glyphOrder = varFont.getGlyphOrder()
|
||||||
convertCFFtoCFF2(varFont)
|
convertCFFtoCFF2(varFont)
|
||||||
ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping)
|
ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping)
|
||||||
# re-ordering the master list simplifies building the CFF2 data item lists.
|
# re-ordering the master list simplifies building the CFF2 data item lists.
|
||||||
addCFFVarStore(varFont, model) # Add VarStore to the CFF2 font.
|
|
||||||
merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
|
merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
|
||||||
|
|
||||||
|
|
||||||
|
522
Lib/fontTools/varLib/cff.py
Normal file → Executable file
522
Lib/fontTools/varLib/cff.py
Normal file → Executable file
@ -1,7 +1,5 @@
|
|||||||
|
from collections import namedtuple
|
||||||
import os
|
import os
|
||||||
from fontTools.misc.py23 import BytesIO
|
|
||||||
from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor
|
|
||||||
from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round
|
|
||||||
from fontTools.cffLib import (
|
from fontTools.cffLib import (
|
||||||
maxStackLimit,
|
maxStackLimit,
|
||||||
TopDictIndex,
|
TopDictIndex,
|
||||||
@ -14,20 +12,21 @@ from fontTools.cffLib import (
|
|||||||
FontDict,
|
FontDict,
|
||||||
VarStoreData
|
VarStoreData
|
||||||
)
|
)
|
||||||
from fontTools.cffLib.specializer import (commandsToProgram, specializeCommands)
|
from fontTools.misc.py23 import BytesIO
|
||||||
|
from fontTools.cffLib.specializer import (
|
||||||
|
specializeCommands, commandsToProgram)
|
||||||
from fontTools.ttLib import newTable
|
from fontTools.ttLib import newTable
|
||||||
from fontTools import varLib
|
from fontTools import varLib
|
||||||
from fontTools.varLib.models import allEqual
|
from fontTools.varLib.models import allEqual
|
||||||
|
from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor
|
||||||
|
from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round
|
||||||
|
|
||||||
|
|
||||||
def addCFFVarStore(varFont, varModel):
|
def addCFFVarStore(varFont, varModel, varDataList, masterSupports):
|
||||||
supports = varModel.supports[1:]
|
|
||||||
fvarTable = varFont['fvar']
|
fvarTable = varFont['fvar']
|
||||||
axisKeys = [axis.axisTag for axis in fvarTable.axes]
|
axisKeys = [axis.axisTag for axis in fvarTable.axes]
|
||||||
varTupleList = varLib.builder.buildVarRegionList(supports, axisKeys)
|
varTupleList = varLib.builder.buildVarRegionList(masterSupports, axisKeys)
|
||||||
varTupleIndexes = list(range(len(supports)))
|
varStoreCFFV = varLib.builder.buildVarStore(varTupleList, varDataList)
|
||||||
varDeltasCFFV = varLib.builder.buildVarData(varTupleIndexes, None, False)
|
|
||||||
varStoreCFFV = varLib.builder.buildVarStore(varTupleList, [varDeltasCFFV])
|
|
||||||
|
|
||||||
topDict = varFont['CFF2'].cff.topDictIndex[0]
|
topDict = varFont['CFF2'].cff.topDictIndex[0]
|
||||||
topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV)
|
topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV)
|
||||||
@ -143,16 +142,61 @@ pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues",
|
|||||||
"StemSnapV")
|
"StemSnapV")
|
||||||
|
|
||||||
|
|
||||||
def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model):
|
def get_private(regionFDArrays, fd_index, ri, fd_map):
|
||||||
|
region_fdArray = regionFDArrays[ri]
|
||||||
|
region_fd_map = fd_map[fd_index]
|
||||||
|
if ri in region_fd_map:
|
||||||
|
region_fdIndex = region_fd_map[ri]
|
||||||
|
private = region_fdArray[region_fdIndex].Private
|
||||||
|
else:
|
||||||
|
private = None
|
||||||
|
return private
|
||||||
|
|
||||||
|
|
||||||
|
def merge_PrivateDicts(top_dicts, vsindex_dict, var_model, fd_map):
|
||||||
|
"""
|
||||||
|
I step through the FontDicts in the FDArray of the varfont TopDict.
|
||||||
|
For each varfont FontDict:
|
||||||
|
step through each key in FontDict.Private.
|
||||||
|
For each key, step through each relevant source font Private dict, and
|
||||||
|
build a list of values to blend.
|
||||||
|
The 'relevant' source fonts are selected by first getting the right
|
||||||
|
submodel using model_keys[vsindex]. The indices of the
|
||||||
|
subModel.locations are mapped to source font list indices by
|
||||||
|
assuming the latter order is the same as the order of the
|
||||||
|
var_model.locations. I can then get the index of each subModel
|
||||||
|
location in the list of var_model.locations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
topDict = top_dicts[0]
|
||||||
|
region_top_dicts = top_dicts[1:]
|
||||||
if hasattr(region_top_dicts[0], 'FDArray'):
|
if hasattr(region_top_dicts[0], 'FDArray'):
|
||||||
regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts]
|
regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts]
|
||||||
else:
|
else:
|
||||||
regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts]
|
regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts]
|
||||||
for fd_index, font_dict in enumerate(topDict.FDArray):
|
for fd_index, font_dict in enumerate(topDict.FDArray):
|
||||||
private_dict = font_dict.Private
|
private_dict = font_dict.Private
|
||||||
pds = [private_dict] + [
|
vsindex = getattr(private_dict, 'vsindex', 0)
|
||||||
regionFDArray[fd_index].Private for regionFDArray in regionFDArrays
|
# At the moment, no PrivateDict has a vsindex key, but let's support
|
||||||
]
|
# how it should work. See comment at end of
|
||||||
|
# merge_charstrings() - still need to optimize use of vsindex.
|
||||||
|
sub_model, model_keys = vsindex_dict[vsindex]
|
||||||
|
master_indices = []
|
||||||
|
for loc in sub_model.locations[1:]:
|
||||||
|
i = var_model.locations.index(loc) - 1
|
||||||
|
master_indices.append(i)
|
||||||
|
pds = [private_dict]
|
||||||
|
last_pd = private_dict
|
||||||
|
for ri in master_indices:
|
||||||
|
pd = get_private(regionFDArrays, fd_index, ri, fd_map)
|
||||||
|
# If the region font doesn't have this FontDict, just reference
|
||||||
|
# the last one used.
|
||||||
|
if pd is None:
|
||||||
|
pd = last_pd
|
||||||
|
else:
|
||||||
|
last_pd = pd
|
||||||
|
pds.append(pd)
|
||||||
|
num_masters = len(pds)
|
||||||
for key, value in private_dict.rawDict.items():
|
for key, value in private_dict.rawDict.items():
|
||||||
if key not in pd_blend_fields:
|
if key not in pd_blend_fields:
|
||||||
continue
|
continue
|
||||||
@ -192,7 +236,7 @@ def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model):
|
|||||||
if (not any_points_differ) and not allEqual(rel_list):
|
if (not any_points_differ) and not allEqual(rel_list):
|
||||||
any_points_differ = True
|
any_points_differ = True
|
||||||
prev_val_list = val_list
|
prev_val_list = val_list
|
||||||
deltas = var_model.getDeltas(rel_list)
|
deltas = sub_model.getDeltas(rel_list)
|
||||||
# Convert numbers with no decimal part to an int.
|
# Convert numbers with no decimal part to an int.
|
||||||
deltas = [conv_to_int(delta) for delta in deltas]
|
deltas = [conv_to_int(delta) for delta in deltas]
|
||||||
# For PrivateDict BlueValues, the default font
|
# For PrivateDict BlueValues, the default font
|
||||||
@ -206,61 +250,159 @@ def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model):
|
|||||||
else:
|
else:
|
||||||
values = [pd.rawDict[key] for pd in pds]
|
values = [pd.rawDict[key] for pd in pds]
|
||||||
if not allEqual(values):
|
if not allEqual(values):
|
||||||
dataList = var_model.getDeltas(values)
|
dataList = sub_model.getDeltas(values)
|
||||||
else:
|
else:
|
||||||
dataList = values[0]
|
dataList = values[0]
|
||||||
private_dict.rawDict[key] = dataList
|
private_dict.rawDict[key] = dataList
|
||||||
|
|
||||||
|
|
||||||
|
def getfd_map(varFont, fonts_list):
|
||||||
|
""" Since a subset source font may have fewer FontDicts in their
|
||||||
|
FDArray than the default font, we have to match up the FontDicts in
|
||||||
|
the different fonts . We do this with the FDSelect array, and by
|
||||||
|
assuming that the same glyph will reference matching FontDicts in
|
||||||
|
each source font. We return a mapping from fdIndex in the default
|
||||||
|
font to a dictionary which maps each master list index of each
|
||||||
|
region font to the equivalent fdIndex in the region font."""
|
||||||
|
fd_map = {}
|
||||||
|
default_font = fonts_list[0]
|
||||||
|
region_fonts = fonts_list[1:]
|
||||||
|
num_regions = len(region_fonts)
|
||||||
|
topDict = default_font['CFF '].cff.topDictIndex[0]
|
||||||
|
if not hasattr(topDict, 'FDSelect'):
|
||||||
|
fd_map[0] = [0]*num_regions
|
||||||
|
return fd_map
|
||||||
|
|
||||||
|
gname_mapping = {}
|
||||||
|
default_fdSelect = topDict.FDSelect
|
||||||
|
glyphOrder = default_font.getGlyphOrder()
|
||||||
|
for gid, fdIndex in enumerate(default_fdSelect):
|
||||||
|
gname_mapping[glyphOrder[gid]] = fdIndex
|
||||||
|
if fdIndex not in fd_map:
|
||||||
|
fd_map[fdIndex] = {}
|
||||||
|
for ri, region_font in enumerate(region_fonts):
|
||||||
|
region_glyphOrder = region_font.getGlyphOrder()
|
||||||
|
region_topDict = region_font['CFF '].cff.topDictIndex[0]
|
||||||
|
if not hasattr(region_topDict, 'FDSelect'):
|
||||||
|
# All the glyphs share the same FontDict. Pick any glyph.
|
||||||
|
default_fdIndex = gname_mapping[region_glyphOrder[0]]
|
||||||
|
fd_map[default_fdIndex][ri] = 0
|
||||||
|
else:
|
||||||
|
region_fdSelect = region_topDict.FDSelect
|
||||||
|
for gid, fdIndex in enumerate(region_fdSelect):
|
||||||
|
default_fdIndex = gname_mapping[region_glyphOrder[gid]]
|
||||||
|
region_map = fd_map[default_fdIndex]
|
||||||
|
if ri not in region_map:
|
||||||
|
region_map[ri] = fdIndex
|
||||||
|
return fd_map
|
||||||
|
|
||||||
|
|
||||||
|
CVarData = namedtuple('CVarData', 'varDataList masterSupports vsindex_dict')
|
||||||
def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder):
|
def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder):
|
||||||
topDict = varFont['CFF2'].cff.topDictIndex[0]
|
topDict = varFont['CFF2'].cff.topDictIndex[0]
|
||||||
default_charstrings = topDict.CharStrings
|
top_dicts = [topDict] + [
|
||||||
region_fonts = ordered_fonts_list[1:]
|
ttFont['CFF '].cff.topDictIndex[0]
|
||||||
region_top_dicts = [
|
for ttFont in ordered_fonts_list[1:]
|
||||||
ttFont['CFF '].cff.topDictIndex[0] for ttFont in region_fonts
|
]
|
||||||
]
|
|
||||||
num_masters = len(model.mapping)
|
num_masters = len(model.mapping)
|
||||||
merge_PrivateDicts(topDict, region_top_dicts, num_masters, model)
|
cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model)
|
||||||
merge_charstrings(default_charstrings,
|
fd_map = getfd_map(varFont, ordered_fonts_list)
|
||||||
glyphOrder,
|
merge_PrivateDicts(top_dicts, cvData.vsindex_dict, model, fd_map)
|
||||||
num_masters,
|
addCFFVarStore(varFont, model, cvData.varDataList,
|
||||||
region_top_dicts, model)
|
cvData.masterSupports)
|
||||||
|
|
||||||
|
|
||||||
def merge_charstrings(default_charstrings,
|
def _get_cs(charstrings, glyphName):
|
||||||
glyphOrder,
|
if glyphName not in charstrings:
|
||||||
num_masters,
|
return None
|
||||||
region_top_dicts,
|
return charstrings[glyphName]
|
||||||
var_model):
|
|
||||||
for gname in glyphOrder:
|
|
||||||
default_charstring = default_charstrings[gname]
|
def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel):
|
||||||
|
|
||||||
|
vsindex_dict = {}
|
||||||
|
vsindex_by_key = {}
|
||||||
|
varDataList = []
|
||||||
|
masterSupports = []
|
||||||
|
default_charstrings = top_dicts[0].CharStrings
|
||||||
|
for gid, gname in enumerate(glyphOrder):
|
||||||
|
all_cs = [
|
||||||
|
_get_cs(td.CharStrings, gname)
|
||||||
|
for td in top_dicts]
|
||||||
|
if len([gs for gs in all_cs if gs is not None]) == 1:
|
||||||
|
continue
|
||||||
|
model, model_cs = masterModel.getSubModel(all_cs)
|
||||||
|
# create the first pass CFF2 charstring, from
|
||||||
|
# the default charstring.
|
||||||
|
default_charstring = model_cs[0]
|
||||||
var_pen = CFF2CharStringMergePen([], gname, num_masters, 0)
|
var_pen = CFF2CharStringMergePen([], gname, num_masters, 0)
|
||||||
default_charstring.outlineExtractor = CFFToCFF2OutlineExtractor
|
# We need to override outlineExtractor because these
|
||||||
|
# charstrings do have widths in the 'program'; we need to drop these
|
||||||
|
# values rather than post assertion error for them.
|
||||||
|
default_charstring.outlineExtractor = MergeOutlineExtractor
|
||||||
default_charstring.draw(var_pen)
|
default_charstring.draw(var_pen)
|
||||||
for region_idx, region_td in enumerate(region_top_dicts, start=1):
|
|
||||||
region_charstrings = region_td.CharStrings
|
# Add the coordinates from all the other regions to the
|
||||||
region_charstring = region_charstrings[gname]
|
# blend lists in the CFF2 charstring.
|
||||||
|
region_cs = model_cs[1:]
|
||||||
|
for region_idx, region_charstring in enumerate(region_cs, start=1):
|
||||||
var_pen.restart(region_idx)
|
var_pen.restart(region_idx)
|
||||||
|
region_charstring.outlineExtractor = MergeOutlineExtractor
|
||||||
region_charstring.draw(var_pen)
|
region_charstring.draw(var_pen)
|
||||||
new_charstring = var_pen.getCharString(
|
|
||||||
|
# Collapse each coordinate list to a blend operator and its args.
|
||||||
|
new_cs = var_pen.getCharString(
|
||||||
private=default_charstring.private,
|
private=default_charstring.private,
|
||||||
globalSubrs=default_charstring.globalSubrs,
|
globalSubrs=default_charstring.globalSubrs,
|
||||||
var_model=var_model, optimize=True)
|
var_model=model, optimize=True)
|
||||||
default_charstrings[gname] = new_charstring
|
default_charstrings[gname] = new_cs
|
||||||
|
|
||||||
|
if (not var_pen.seen_moveto) or ('blend' not in new_cs.program):
|
||||||
|
# If this is not a marking glyph, or if there are no blend
|
||||||
|
# arguments, then we can use vsindex 0. No need to
|
||||||
|
# check if we need a new vsindex.
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If the charstring required a new model, create
|
||||||
|
# a VarData table to go with, and set vsindex.
|
||||||
|
try:
|
||||||
|
key = tuple(v is not None for v in all_cs)
|
||||||
|
vsindex = vsindex_by_key[key]
|
||||||
|
except KeyError:
|
||||||
|
varTupleIndexes = []
|
||||||
|
for support in model.supports[1:]:
|
||||||
|
if support not in masterSupports:
|
||||||
|
masterSupports.append(support)
|
||||||
|
varTupleIndexes.append(masterSupports.index(support))
|
||||||
|
var_data = varLib.builder.buildVarData(varTupleIndexes, None, False)
|
||||||
|
vsindex = len(vsindex_dict)
|
||||||
|
vsindex_by_key[key] = vsindex
|
||||||
|
vsindex_dict[vsindex] = (model, [key])
|
||||||
|
varDataList.append(var_data)
|
||||||
|
# We do not need to check for an existing new_cs.private.vsindex,
|
||||||
|
# as we know it doesn't exist yet.
|
||||||
|
if vsindex != 0:
|
||||||
|
new_cs.program[:0] = [vsindex, 'vsindex']
|
||||||
|
cvData = CVarData(varDataList=varDataList, masterSupports=masterSupports,
|
||||||
|
vsindex_dict=vsindex_dict)
|
||||||
|
# XXX To do: optimize use of vsindex between the PrivateDicts and
|
||||||
|
# charstrings
|
||||||
|
return cvData
|
||||||
|
|
||||||
|
|
||||||
class MergeTypeError(TypeError):
|
class MergeTypeError(TypeError):
|
||||||
def __init__(self, point_type, pt_index, m_index, default_type, glyphName):
|
def __init__(self, point_type, pt_index, m_index, default_type, glyphName):
|
||||||
self.error_msg = [
|
self.error_msg = [
|
||||||
"In glyph '{gname}' "
|
"In glyph '{gname}' "
|
||||||
"'{point_type}' at point index {pt_index} in master "
|
"'{point_type}' at point index {pt_index} in master "
|
||||||
"index {m_index} differs from the default font point "
|
"index {m_index} differs from the default font point "
|
||||||
"type '{default_type}'"
|
"type '{default_type}'"
|
||||||
"".format(gname=glyphName,
|
"".format(
|
||||||
point_type=point_type, pt_index=pt_index,
|
gname=glyphName,
|
||||||
m_index=m_index, default_type=default_type)
|
point_type=point_type, pt_index=pt_index,
|
||||||
][0]
|
m_index=m_index, default_type=default_type)
|
||||||
super(MergeTypeError, self).__init__(self.error_msg)
|
][0]
|
||||||
|
super(MergeTypeError, self).__init__(self.error_msg)
|
||||||
|
|
||||||
|
|
||||||
def makeRoundNumberFunc(tolerance):
|
def makeRoundNumberFunc(tolerance):
|
||||||
@ -274,10 +416,9 @@ def makeRoundNumberFunc(tolerance):
|
|||||||
|
|
||||||
|
|
||||||
class CFFToCFF2OutlineExtractor(T2OutlineExtractor):
|
class CFFToCFF2OutlineExtractor(T2OutlineExtractor):
|
||||||
""" This class is used to remove the initial width
|
""" This class is used to remove the initial width from the CFF
|
||||||
from the CFF charstring without adding the width
|
charstring without trying to add the width to self.nominalWidthX,
|
||||||
to self.nominalWidthX, which is None.
|
which is None. """
|
||||||
"""
|
|
||||||
def popallWidth(self, evenOdd=0):
|
def popallWidth(self, evenOdd=0):
|
||||||
args = self.popall()
|
args = self.popall()
|
||||||
if not self.gotWidth:
|
if not self.gotWidth:
|
||||||
@ -288,60 +429,127 @@ class CFFToCFF2OutlineExtractor(T2OutlineExtractor):
|
|||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
class MergeOutlineExtractor(CFFToCFF2OutlineExtractor):
|
||||||
|
""" Used to extract the charstring commands - including hints - from a
|
||||||
|
CFF charstring in order to merge it as another set of region data
|
||||||
|
into a CFF2 variable font charstring."""
|
||||||
|
|
||||||
|
def __init__(self, pen, localSubrs, globalSubrs,
|
||||||
|
nominalWidthX, defaultWidthX, private=None):
|
||||||
|
super(CFFToCFF2OutlineExtractor, self).__init__(pen, localSubrs,
|
||||||
|
globalSubrs, nominalWidthX, defaultWidthX, private)
|
||||||
|
|
||||||
|
def countHints(self):
|
||||||
|
args = self.popallWidth()
|
||||||
|
self.hintCount = self.hintCount + len(args) // 2
|
||||||
|
return args
|
||||||
|
|
||||||
|
def _hint_op(self, type, args):
|
||||||
|
self.pen.add_hint(type, args)
|
||||||
|
|
||||||
|
def op_hstem(self, index):
|
||||||
|
args = self.countHints()
|
||||||
|
self._hint_op('hstem', args)
|
||||||
|
|
||||||
|
def op_vstem(self, index):
|
||||||
|
args = self.countHints()
|
||||||
|
self._hint_op('vstem', args)
|
||||||
|
|
||||||
|
def op_hstemhm(self, index):
|
||||||
|
args = self.countHints()
|
||||||
|
self._hint_op('hstemhm', args)
|
||||||
|
|
||||||
|
def op_vstemhm(self, index):
|
||||||
|
args = self.countHints()
|
||||||
|
self._hint_op('vstemhm', args)
|
||||||
|
|
||||||
|
def _get_hintmask(self, index):
|
||||||
|
if not self.hintMaskBytes:
|
||||||
|
args = self.countHints()
|
||||||
|
if args:
|
||||||
|
self._hint_op('vstemhm', args)
|
||||||
|
self.hintMaskBytes = (self.hintCount + 7) // 8
|
||||||
|
hintMaskBytes, index = self.callingStack[-1].getBytes(index,
|
||||||
|
self.hintMaskBytes)
|
||||||
|
return index, hintMaskBytes
|
||||||
|
|
||||||
|
def op_hintmask(self, index):
|
||||||
|
index, hintMaskBytes = self._get_hintmask(index)
|
||||||
|
self.pen.add_hintmask('hintmask', [hintMaskBytes])
|
||||||
|
return hintMaskBytes, index
|
||||||
|
|
||||||
|
def op_cntrmask(self, index):
|
||||||
|
index, hintMaskBytes = self._get_hintmask(index)
|
||||||
|
self.pen.add_hintmask('cntrmask', [hintMaskBytes])
|
||||||
|
return hintMaskBytes, index
|
||||||
|
|
||||||
|
|
||||||
class CFF2CharStringMergePen(T2CharStringPen):
|
class CFF2CharStringMergePen(T2CharStringPen):
|
||||||
"""Pen to merge Type 2 CharStrings.
|
"""Pen to merge Type 2 CharStrings.
|
||||||
"""
|
"""
|
||||||
def __init__(self, default_commands,
|
def __init__(
|
||||||
glyphName, num_masters, master_idx, roundTolerance=0.5):
|
self, default_commands, glyphName, num_masters, master_idx,
|
||||||
|
roundTolerance=0.5):
|
||||||
super(
|
super(
|
||||||
CFF2CharStringMergePen,
|
CFF2CharStringMergePen,
|
||||||
self).__init__(width=None,
|
self).__init__(
|
||||||
glyphSet=None, CFF2=True,
|
width=None,
|
||||||
roundTolerance=roundTolerance)
|
glyphSet=None, CFF2=True,
|
||||||
|
roundTolerance=roundTolerance)
|
||||||
self.pt_index = 0
|
self.pt_index = 0
|
||||||
self._commands = default_commands
|
self._commands = default_commands
|
||||||
self.m_index = master_idx
|
self.m_index = master_idx
|
||||||
self.num_masters = num_masters
|
self.num_masters = num_masters
|
||||||
self.prev_move_idx = 0
|
self.prev_move_idx = 0
|
||||||
|
self.seen_moveto = False
|
||||||
self.glyphName = glyphName
|
self.glyphName = glyphName
|
||||||
self.roundNumber = makeRoundNumberFunc(roundTolerance)
|
self.roundNumber = makeRoundNumberFunc(roundTolerance)
|
||||||
|
|
||||||
def _p(self, pt):
|
|
||||||
""" Unlike T2CharstringPen, this class stores absolute values.
|
|
||||||
This is to allow the logic in check_and_fix_closepath() to work,
|
|
||||||
where the current or previous absolute point has to be compared to
|
|
||||||
the path start-point.
|
|
||||||
"""
|
|
||||||
self._p0 = pt
|
|
||||||
return list(self._p0)
|
|
||||||
|
|
||||||
def add_point(self, point_type, pt_coords):
|
def add_point(self, point_type, pt_coords):
|
||||||
if self.m_index == 0:
|
if self.m_index == 0:
|
||||||
self._commands.append([point_type, [pt_coords]])
|
self._commands.append([point_type, [pt_coords]])
|
||||||
else:
|
else:
|
||||||
cmd = self._commands[self.pt_index]
|
cmd = self._commands[self.pt_index]
|
||||||
if cmd[0] != point_type:
|
if cmd[0] != point_type:
|
||||||
# Fix some issues that show up in some
|
raise MergeTypeError(
|
||||||
# CFF workflows, even when fonts are
|
point_type,
|
||||||
# topologically merge compatible.
|
self.pt_index, len(cmd[1]),
|
||||||
success, pt_coords = self.check_and_fix_flat_curve(
|
cmd[0], self.glyphName)
|
||||||
cmd, point_type, pt_coords)
|
|
||||||
if not success:
|
|
||||||
success = self.check_and_fix_closepath(
|
|
||||||
cmd, point_type, pt_coords)
|
|
||||||
if success:
|
|
||||||
# We may have incremented self.pt_index
|
|
||||||
cmd = self._commands[self.pt_index]
|
|
||||||
if cmd[0] != point_type:
|
|
||||||
success = False
|
|
||||||
if not success:
|
|
||||||
raise MergeTypeError(point_type,
|
|
||||||
self.pt_index, len(cmd[1]),
|
|
||||||
cmd[0], self.glyphName)
|
|
||||||
cmd[1].append(pt_coords)
|
cmd[1].append(pt_coords)
|
||||||
self.pt_index += 1
|
self.pt_index += 1
|
||||||
|
|
||||||
|
def add_hint(self, hint_type, args):
|
||||||
|
if self.m_index == 0:
|
||||||
|
self._commands.append([hint_type, [args]])
|
||||||
|
else:
|
||||||
|
cmd = self._commands[self.pt_index]
|
||||||
|
if cmd[0] != hint_type:
|
||||||
|
raise MergeTypeError(hint_type, self.pt_index, len(cmd[1]),
|
||||||
|
cmd[0], self.glyphName)
|
||||||
|
cmd[1].append(args)
|
||||||
|
self.pt_index += 1
|
||||||
|
|
||||||
|
def add_hintmask(self, hint_type, abs_args):
|
||||||
|
# For hintmask, fonttools.cffLib.specializer.py expects
|
||||||
|
# each of these to be represented by two sequential commands:
|
||||||
|
# first holding only the operator name, with an empty arg list,
|
||||||
|
# second with an empty string as the op name, and the mask arg list.
|
||||||
|
if self.m_index == 0:
|
||||||
|
self._commands.append([hint_type, []])
|
||||||
|
self._commands.append(["", [abs_args]])
|
||||||
|
else:
|
||||||
|
cmd = self._commands[self.pt_index]
|
||||||
|
if cmd[0] != hint_type:
|
||||||
|
raise MergeTypeError(hint_type, self.pt_index, len(cmd[1]),
|
||||||
|
cmd[0], self.glyphName)
|
||||||
|
self.pt_index += 1
|
||||||
|
cmd = self._commands[self.pt_index]
|
||||||
|
cmd[1].append(abs_args)
|
||||||
|
self.pt_index += 1
|
||||||
|
|
||||||
def _moveTo(self, pt):
|
def _moveTo(self, pt):
|
||||||
|
if not self.seen_moveto:
|
||||||
|
self.seen_moveto = True
|
||||||
pt_coords = self._p(pt)
|
pt_coords = self._p(pt)
|
||||||
self.add_point('rmoveto', pt_coords)
|
self.add_point('rmoveto', pt_coords)
|
||||||
# I set prev_move_idx here because add_point()
|
# I set prev_move_idx here because add_point()
|
||||||
@ -371,7 +579,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
|
|||||||
def getCommands(self):
|
def getCommands(self):
|
||||||
return self._commands
|
return self._commands
|
||||||
|
|
||||||
def reorder_blend_args(self, commands):
|
def reorder_blend_args(self, commands, get_delta_func, round_func):
|
||||||
"""
|
"""
|
||||||
We first re-order the master coordinate values.
|
We first re-order the master coordinate values.
|
||||||
For a moveto to lineto, the args are now arranged as:
|
For a moveto to lineto, the args are now arranged as:
|
||||||
@ -380,9 +588,13 @@ class CFF2CharStringMergePen(T2CharStringPen):
|
|||||||
[ [master_0 x, master_1 x, master_2 x],
|
[ [master_0 x, master_1 x, master_2 x],
|
||||||
[master_0 y, master_1 y, master_2 y]
|
[master_0 y, master_1 y, master_2 y]
|
||||||
]
|
]
|
||||||
We also make the value relative.
|
|
||||||
If the master values are all the same, we collapse the list to
|
If the master values are all the same, we collapse the list to
|
||||||
as single value instead of a list.
|
as single value instead of a list.
|
||||||
|
|
||||||
|
We then convert this to:
|
||||||
|
[ [master_0 x] + [x delta tuple] + [numBlends=1]
|
||||||
|
[master_0 y] + [y delta tuple] + [numBlends=1]
|
||||||
|
]
|
||||||
"""
|
"""
|
||||||
for cmd in commands:
|
for cmd in commands:
|
||||||
# arg[i] is the set of arguments for this operator from master i.
|
# arg[i] is the set of arguments for this operator from master i.
|
||||||
@ -390,108 +602,46 @@ class CFF2CharStringMergePen(T2CharStringPen):
|
|||||||
m_args = zip(*args)
|
m_args = zip(*args)
|
||||||
# m_args[n] is now all num_master args for the i'th argument
|
# m_args[n] is now all num_master args for the i'th argument
|
||||||
# for this operation.
|
# for this operation.
|
||||||
cmd[1] = m_args
|
cmd[1] = list(m_args)
|
||||||
|
lastOp = None
|
||||||
# Now convert from absolute to relative
|
for cmd in commands:
|
||||||
x0 = [0]*self.num_masters
|
op = cmd[0]
|
||||||
y0 = [0]*self.num_masters
|
# masks are represented by two cmd's: first has only op names,
|
||||||
for cmd in self._commands:
|
# second has only args.
|
||||||
is_x = True
|
if lastOp in ['hintmask', 'cntrmask']:
|
||||||
coords = cmd[1]
|
coord = list(cmd[1])
|
||||||
rel_coords = []
|
assert allEqual(coord), (
|
||||||
for coord in coords:
|
"hintmask values cannot differ between source fonts.")
|
||||||
prev_coord = x0 if is_x else y0
|
cmd[1] = [coord[0][0]]
|
||||||
rel_coord = [pt[0] - pt[1] for pt in zip(coord, prev_coord)]
|
else:
|
||||||
|
coords = cmd[1]
|
||||||
if allEqual(rel_coord):
|
new_coords = []
|
||||||
rel_coord = rel_coord[0]
|
for coord in coords:
|
||||||
rel_coords.append(rel_coord)
|
if allEqual(coord):
|
||||||
if is_x:
|
new_coords.append(coord[0])
|
||||||
x0 = coord
|
else:
|
||||||
else:
|
# convert to deltas
|
||||||
y0 = coord
|
deltas = get_delta_func(coord)[1:]
|
||||||
is_x = not is_x
|
|
||||||
cmd[1] = rel_coords
|
|
||||||
return commands
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mergeCommandsToProgram(commands, var_model, round_func):
|
|
||||||
"""
|
|
||||||
Takes a commands list as returned by programToCommands() and
|
|
||||||
converts it back to a T2CharString or CFF2Charstring program list. I
|
|
||||||
need to use this rather than specialize.commandsToProgram, as the
|
|
||||||
commands produced by CFF2CharStringMergePen initially contains a
|
|
||||||
list of coordinate values, one for each master, wherever a single
|
|
||||||
coordinate value is expected by the regular logic. The problem with
|
|
||||||
doing using the specialize.py functions is that a commands list is
|
|
||||||
expected to be a op name with its associated argument list. For the
|
|
||||||
commands list here, some of the arguments may need to be converted
|
|
||||||
to a new argument list and opcode.
|
|
||||||
This version will convert each list of master arguments to a blend
|
|
||||||
op and its arguments, and will also combine successive blend ops up
|
|
||||||
to the stack limit.
|
|
||||||
"""
|
|
||||||
program = []
|
|
||||||
for op, args in commands:
|
|
||||||
num_args = len(args)
|
|
||||||
# some of the args may be blend lists, and some may be
|
|
||||||
# single coordinate values.
|
|
||||||
i = 0
|
|
||||||
stack_use = 0
|
|
||||||
while i < num_args:
|
|
||||||
arg = args[i]
|
|
||||||
if not isinstance(arg, list):
|
|
||||||
program.append(arg)
|
|
||||||
i += 1
|
|
||||||
stack_use += 1
|
|
||||||
else:
|
|
||||||
prev_stack_use = stack_use
|
|
||||||
""" The arg is a tuple of blend values.
|
|
||||||
These are each (master 0,master 1..master n)
|
|
||||||
Combine as many successive tuples as we can,
|
|
||||||
up to the max stack limit.
|
|
||||||
"""
|
|
||||||
num_masters = len(arg)
|
|
||||||
blendlist = [arg]
|
|
||||||
i += 1
|
|
||||||
stack_use += 1 + num_masters # 1 for the num_blends arg
|
|
||||||
while (i < num_args) and isinstance(args[i], list):
|
|
||||||
blendlist.append(args[i])
|
|
||||||
i += 1
|
|
||||||
stack_use += num_masters
|
|
||||||
if stack_use + num_masters > maxStackLimit:
|
|
||||||
# if we are here, max stack is is the CFF2 max stack.
|
|
||||||
break
|
|
||||||
num_blends = len(blendlist)
|
|
||||||
# append the 'num_blends' default font values
|
|
||||||
for arg in blendlist:
|
|
||||||
if round_func:
|
|
||||||
arg[0] = round_func(arg[0])
|
|
||||||
program.append(arg[0])
|
|
||||||
for arg in blendlist:
|
|
||||||
deltas = var_model.getDeltas(arg)
|
|
||||||
if round_func:
|
if round_func:
|
||||||
deltas = [round_func(delta) for delta in deltas]
|
deltas = [round_func(delta) for delta in deltas]
|
||||||
# First item in 'deltas' is the default master value;
|
coord = [coord[0]] + deltas
|
||||||
# for CFF2 data, that has already been written.
|
new_coords.append(coord)
|
||||||
program.extend(deltas[1:])
|
cmd[1] = new_coords
|
||||||
program.append(num_blends)
|
lastOp = op
|
||||||
program.append('blend')
|
return commands
|
||||||
stack_use = prev_stack_use + num_blends
|
|
||||||
if op:
|
|
||||||
program.append(op)
|
|
||||||
return program
|
|
||||||
|
|
||||||
|
def getCharString(
|
||||||
def getCharString(self, private=None, globalSubrs=None,
|
self, private=None, globalSubrs=None,
|
||||||
var_model=None, optimize=True):
|
var_model=None, optimize=True):
|
||||||
commands = self._commands
|
commands = self._commands
|
||||||
commands = self.reorder_blend_args(commands)
|
commands = self.reorder_blend_args(commands, var_model.getDeltas,
|
||||||
|
self.roundNumber)
|
||||||
if optimize:
|
if optimize:
|
||||||
commands = specializeCommands(commands, generalizeFirst=False,
|
commands = specializeCommands(
|
||||||
maxstack=maxStackLimit)
|
commands, generalizeFirst=False,
|
||||||
program = self.mergeCommandsToProgram(commands, var_model=var_model,
|
maxstack=maxStackLimit)
|
||||||
round_func=self.roundNumber)
|
program = commandsToProgram(commands)
|
||||||
charString = T2CharString(program=program, private=private,
|
charString = T2CharString(
|
||||||
globalSubrs=globalSubrs)
|
program=program, private=private,
|
||||||
|
globalSubrs=globalSubrs)
|
||||||
return charString
|
return charString
|
||||||
|
@ -64,24 +64,29 @@ def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
|
|||||||
charstrings = topDict.CharStrings
|
charstrings = topDict.CharStrings
|
||||||
for gname in glyphOrder:
|
for gname in glyphOrder:
|
||||||
# Interpolate charstring
|
# Interpolate charstring
|
||||||
|
# e.g replace blend op args with regular args,
|
||||||
|
# and use and discard vsindex op.
|
||||||
charstring = charstrings[gname]
|
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 = []
|
new_program = []
|
||||||
|
vsindex = 0
|
||||||
last_i = 0
|
last_i = 0
|
||||||
for i, token in enumerate(charstring.program):
|
for i, token in enumerate(charstring.program):
|
||||||
if token == 'blend':
|
if token == 'vsindex':
|
||||||
|
vsindex = charstring.program[i - 1]
|
||||||
|
if last_i != 0:
|
||||||
|
new_program.extend(charstring.program[last_i:i - 1])
|
||||||
|
last_i = i + 1
|
||||||
|
elif token == 'blend':
|
||||||
|
num_regions = charstring.getNumRegions(vsindex)
|
||||||
|
numMasters = 1 + num_regions
|
||||||
num_args = charstring.program[i - 1]
|
num_args = charstring.program[i - 1]
|
||||||
""" The stack is now:
|
# The program list starting at program[i] is now:
|
||||||
..args for following operations
|
# ..args for following operations
|
||||||
num_args values from the default font
|
# num_args values from the default font
|
||||||
num_args tuples, each with numMasters-1 delta values
|
# num_args tuples, each with numMasters-1 delta values
|
||||||
num_blend_args
|
# num_blend_args
|
||||||
'blend'
|
# 'blend'
|
||||||
"""
|
argi = i - (num_args * numMasters + 1)
|
||||||
argi = i - (num_args*numMasters + 1)
|
|
||||||
end_args = tuplei = argi + num_args
|
end_args = tuplei = argi + num_args
|
||||||
while argi < end_args:
|
while argi < end_args:
|
||||||
next_ti = tuplei + num_regions
|
next_ti = tuplei + num_regions
|
||||||
|
@ -190,8 +190,10 @@ class VarStoreInstancer(object):
|
|||||||
#
|
#
|
||||||
# Optimizations
|
# Optimizations
|
||||||
#
|
#
|
||||||
|
# retainFirstMap - If true, major 0 mappings are retained. Deltas for unused indices are zeroed
|
||||||
|
# advIdxes - Set of major 0 indices for advance deltas to be listed first. Other major 0 indices follow.
|
||||||
|
|
||||||
def VarStore_subset_varidxes(self, varIdxes, optimize=True):
|
def VarStore_subset_varidxes(self, varIdxes, optimize=True, retainFirstMap=False, advIdxes=set()):
|
||||||
|
|
||||||
# Sort out used varIdxes by major/minor.
|
# Sort out used varIdxes by major/minor.
|
||||||
used = {}
|
used = {}
|
||||||
@ -220,10 +222,19 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True):
|
|||||||
|
|
||||||
items = data.Item
|
items = data.Item
|
||||||
newItems = []
|
newItems = []
|
||||||
for minor in sorted(usedMinors):
|
if major == 0 and retainFirstMap:
|
||||||
newMinor = len(newItems)
|
for minor in range(len(items)):
|
||||||
newItems.append(items[minor])
|
newItems.append(items[minor] if minor in usedMinors else [0] * len(items[minor]))
|
||||||
varDataMap[(major<<16)+minor] = (newMajor<<16)+newMinor
|
varDataMap[minor] = minor
|
||||||
|
else:
|
||||||
|
if major == 0:
|
||||||
|
minors = sorted(advIdxes) + sorted(usedMinors - advIdxes)
|
||||||
|
else:
|
||||||
|
minors = sorted(usedMinors)
|
||||||
|
for minor in minors:
|
||||||
|
newMinor = len(newItems)
|
||||||
|
newItems.append(items[minor])
|
||||||
|
varDataMap[(major<<16)+minor] = (newMajor<<16)+newMinor
|
||||||
|
|
||||||
data.Item = newItems
|
data.Item = newItems
|
||||||
data.ItemCount = len(data.Item)
|
data.ItemCount = len(data.Item)
|
||||||
|
18
NEWS.rst
18
NEWS.rst
@ -1,3 +1,21 @@
|
|||||||
|
3.41.0 (released 2019-04-29)
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
- [varLib/cffLib] Added support for building ``CFF2`` variable font from sparse
|
||||||
|
masters, or masters with more than one model (multiple ``VarStore.VarData``).
|
||||||
|
In ``cffLib.specializer``, added support for ``CFF2`` CharStrings with
|
||||||
|
``blend`` operators (#1547, #1591).
|
||||||
|
- [subset] Fixed subsetting ``HVAR`` and ``VVAR`` with ``--retain-gids`` option,
|
||||||
|
and when advances mapping is null while sidebearings mappings are non-null
|
||||||
|
(#1587, #1588).
|
||||||
|
- Added ``otlLib.maxContextCalc`` module to compute ``OS/2.usMaxContext`` value.
|
||||||
|
Calculate it automatically when compiling features with feaLib. Added option
|
||||||
|
``--recalc-max-context`` to ``subset`` module (#1582).
|
||||||
|
- [otBase/otTables] Fixed ``AttributeError`` on missing OT table fields after
|
||||||
|
importing font from TTX (#1584).
|
||||||
|
- [Silf] Fixed typo ``Silf`` table's ``decompile`` method (#1586).
|
||||||
|
- [otlLib] Better compress ``GPOS`` SinglePos (LookupType 1) subtables (#1539).
|
||||||
|
|
||||||
3.40.0 (released 2019-04-08)
|
3.40.0 (released 2019-04-08)
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
BIN
Tests/cffLib/data/TestSparseCFF2VF.otf
Normal file
BIN
Tests/cffLib/data/TestSparseCFF2VF.otf
Normal file
Binary file not shown.
@ -1,6 +1,11 @@
|
|||||||
from __future__ import print_function, division, absolute_import
|
from __future__ import print_function, division, absolute_import
|
||||||
from fontTools.cffLib.specializer import (programToString, stringToProgram,
|
from fontTools.cffLib.specializer import (programToString, stringToProgram,
|
||||||
generalizeProgram, specializeProgram)
|
generalizeProgram, specializeProgram,
|
||||||
|
programToCommands, commandsToProgram,
|
||||||
|
generalizeCommands,
|
||||||
|
specializeCommands)
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
@ -913,6 +918,42 @@ class CFFSpecializeProgramTest(unittest.TestCase):
|
|||||||
self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr)
|
self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr)
|
||||||
|
|
||||||
|
|
||||||
|
class CFF2VFTestSpecialize(unittest.TestCase):
|
||||||
|
|
||||||
|
def __init__(self, methodName):
|
||||||
|
unittest.TestCase.__init__(self, methodName)
|
||||||
|
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
|
||||||
|
# and fires deprecation warnings if a program uses the old name.
|
||||||
|
if not hasattr(self, "assertRaisesRegex"):
|
||||||
|
self.assertRaisesRegex = self.assertRaisesRegexp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_test_input(test_file_or_folder):
|
||||||
|
path, _ = os.path.split(__file__)
|
||||||
|
return os.path.join(path, "data", test_file_or_folder)
|
||||||
|
|
||||||
|
def test_blend_round_trip(self):
|
||||||
|
otfvf_path = self.get_test_input('TestSparseCFF2VF.otf')
|
||||||
|
ttf_font = TTFont(otfvf_path)
|
||||||
|
fontGlyphList = ttf_font.getGlyphOrder()
|
||||||
|
topDict = ttf_font['CFF2'].cff.topDictIndex[0]
|
||||||
|
charstrings = topDict.CharStrings
|
||||||
|
for glyphName in fontGlyphList:
|
||||||
|
print(glyphName)
|
||||||
|
cs = charstrings[glyphName]
|
||||||
|
cs.decompile()
|
||||||
|
cmds = programToCommands(cs.program, getNumRegions=cs.getNumRegions)
|
||||||
|
cmds_g = generalizeCommands(cmds)
|
||||||
|
cmds = specializeCommands(cmds_g, generalizeFirst=False)
|
||||||
|
program = commandsToProgram(cmds)
|
||||||
|
self.assertEqual(program, cs.program)
|
||||||
|
program = specializeProgram(program, getNumRegions=cs.getNumRegions)
|
||||||
|
self.assertEqual(program, cs.program)
|
||||||
|
program_g = generalizeProgram(program, getNumRegions=cs.getNumRegions)
|
||||||
|
program = commandsToProgram(cmds_g)
|
||||||
|
self.assertEqual(program, program_g)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
sys.exit(unittest.main())
|
sys.exit(unittest.main())
|
||||||
|
@ -233,6 +233,57 @@
|
|||||||
</GlobalSubrs>
|
</GlobalSubrs>
|
||||||
</CFF>
|
</CFF>
|
||||||
|
|
||||||
|
<GPOS>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<ScriptList>
|
||||||
|
<!-- ScriptCount=1 -->
|
||||||
|
<ScriptRecord index="0">
|
||||||
|
<ScriptTag value="DFLT"/>
|
||||||
|
<Script>
|
||||||
|
<DefaultLangSys>
|
||||||
|
<ReqFeatureIndex value="65535"/>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureIndex index="0" value="0"/>
|
||||||
|
</DefaultLangSys>
|
||||||
|
<!-- LangSysCount=0 -->
|
||||||
|
</Script>
|
||||||
|
</ScriptRecord>
|
||||||
|
</ScriptList>
|
||||||
|
<FeatureList>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureRecord index="0">
|
||||||
|
<FeatureTag value="kern"/>
|
||||||
|
<Feature>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<LookupListIndex index="0" value="0"/>
|
||||||
|
</Feature>
|
||||||
|
</FeatureRecord>
|
||||||
|
</FeatureList>
|
||||||
|
<LookupList>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<Lookup index="0">
|
||||||
|
<LookupType value="2"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<PairPos index="0" Format="1">
|
||||||
|
<Coverage Format="1">
|
||||||
|
<Glyph value="A"/>
|
||||||
|
</Coverage>
|
||||||
|
<ValueFormat1 value="4"/>
|
||||||
|
<ValueFormat2 value="0"/>
|
||||||
|
<!-- PairSetCount=1 -->
|
||||||
|
<PairSet index="0">
|
||||||
|
<!-- PairValueCount=1 -->
|
||||||
|
<PairValueRecord index="0">
|
||||||
|
<SecondGlyph value="a"/>
|
||||||
|
<Value1 XAdvance="-50"/>
|
||||||
|
</PairValueRecord>
|
||||||
|
</PairSet>
|
||||||
|
</PairPos>
|
||||||
|
</Lookup>
|
||||||
|
</LookupList>
|
||||||
|
</GPOS>
|
||||||
|
|
||||||
<hmtx>
|
<hmtx>
|
||||||
<mtx name=".notdef" width="600" lsb="100"/>
|
<mtx name=".notdef" width="600" lsb="100"/>
|
||||||
<mtx name=".null" width="600" lsb="100"/>
|
<mtx name=".null" width="600" lsb="100"/>
|
||||||
|
@ -119,7 +119,7 @@
|
|||||||
<sCapHeight value="0"/>
|
<sCapHeight value="0"/>
|
||||||
<usDefaultChar value="0"/>
|
<usDefaultChar value="0"/>
|
||||||
<usBreakChar value="32"/>
|
<usBreakChar value="32"/>
|
||||||
<usMaxContext value="2"/>
|
<usMaxContext value="1"/>
|
||||||
</OS_2>
|
</OS_2>
|
||||||
|
|
||||||
<hmtx>
|
<hmtx>
|
||||||
@ -257,6 +257,45 @@
|
|||||||
</extraNames>
|
</extraNames>
|
||||||
</post>
|
</post>
|
||||||
|
|
||||||
|
<GSUB>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<ScriptList>
|
||||||
|
<!-- ScriptCount=1 -->
|
||||||
|
<ScriptRecord index="0">
|
||||||
|
<ScriptTag value="DFLT"/>
|
||||||
|
<Script>
|
||||||
|
<DefaultLangSys>
|
||||||
|
<ReqFeatureIndex value="65535"/>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureIndex index="0" value="0"/>
|
||||||
|
</DefaultLangSys>
|
||||||
|
<!-- LangSysCount=0 -->
|
||||||
|
</Script>
|
||||||
|
</ScriptRecord>
|
||||||
|
</ScriptList>
|
||||||
|
<FeatureList>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureRecord index="0">
|
||||||
|
<FeatureTag value="salt"/>
|
||||||
|
<Feature>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<LookupListIndex index="0" value="0"/>
|
||||||
|
</Feature>
|
||||||
|
</FeatureRecord>
|
||||||
|
</FeatureList>
|
||||||
|
<LookupList>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<Lookup index="0">
|
||||||
|
<LookupType value="1"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<SingleSubst index="0" Format="1">
|
||||||
|
<Substitution in="A" out="a"/>
|
||||||
|
</SingleSubst>
|
||||||
|
</Lookup>
|
||||||
|
</LookupList>
|
||||||
|
</GSUB>
|
||||||
|
|
||||||
<DSIG>
|
<DSIG>
|
||||||
<!-- note that the Digital Signature will be invalid after recompilation! -->
|
<!-- note that the Digital Signature will be invalid after recompilation! -->
|
||||||
<tableHeader flag="0x1" numSigs="1" version="1"/>
|
<tableHeader flag="0x1" numSigs="1" version="1"/>
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
<sCapHeight value="0"/>
|
<sCapHeight value="0"/>
|
||||||
<usDefaultChar value="0"/>
|
<usDefaultChar value="0"/>
|
||||||
<usBreakChar value="32"/>
|
<usBreakChar value="32"/>
|
||||||
<usMaxContext value="2"/>
|
<usMaxContext value="0"/>
|
||||||
</OS_2>
|
</OS_2>
|
||||||
|
|
||||||
<hmtx>
|
<hmtx>
|
||||||
|
@ -119,7 +119,7 @@
|
|||||||
<sCapHeight value="0"/>
|
<sCapHeight value="0"/>
|
||||||
<usDefaultChar value="0"/>
|
<usDefaultChar value="0"/>
|
||||||
<usBreakChar value="32"/>
|
<usBreakChar value="32"/>
|
||||||
<usMaxContext value="2"/>
|
<usMaxContext value="0"/>
|
||||||
</OS_2>
|
</OS_2>
|
||||||
|
|
||||||
<hmtx>
|
<hmtx>
|
||||||
|
@ -117,6 +117,7 @@ def test_build_ttf(tmpdir):
|
|||||||
fb.setupHorizontalHeader(ascent=824, descent=200)
|
fb.setupHorizontalHeader(ascent=824, descent=200)
|
||||||
fb.setupNameTable(nameStrings)
|
fb.setupNameTable(nameStrings)
|
||||||
fb.setupOS2()
|
fb.setupOS2()
|
||||||
|
fb.addOpenTypeFeatures("feature salt { sub A by a; } salt;")
|
||||||
fb.setupPost()
|
fb.setupPost()
|
||||||
fb.setupDummyDSIG()
|
fb.setupDummyDSIG()
|
||||||
|
|
||||||
@ -145,6 +146,7 @@ def test_build_otf(tmpdir):
|
|||||||
fb.setupHorizontalHeader(ascent=824, descent=200)
|
fb.setupHorizontalHeader(ascent=824, descent=200)
|
||||||
fb.setupNameTable(nameStrings)
|
fb.setupNameTable(nameStrings)
|
||||||
fb.setupOS2()
|
fb.setupOS2()
|
||||||
|
fb.addOpenTypeFeatures("feature kern { pos A a -50; } kern;")
|
||||||
fb.setupPost()
|
fb.setupPost()
|
||||||
fb.setupDummyDSIG()
|
fb.setupDummyDSIG()
|
||||||
|
|
||||||
|
99
Tests/otlLib/data/gpos_91.ttx
Normal file
99
Tests/otlLib/data/gpos_91.ttx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
|
||||||
|
|
||||||
|
<GlyphOrder>
|
||||||
|
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||||
|
<GlyphID id="0" name=".notdef"/>
|
||||||
|
<GlyphID id="1" name="A"/>
|
||||||
|
</GlyphOrder>
|
||||||
|
|
||||||
|
<maxp>
|
||||||
|
<!-- Most of this table will be recalculated by the compiler -->
|
||||||
|
<tableVersion value="0x10000"/>
|
||||||
|
<numGlyphs value="2"/>
|
||||||
|
<maxPoints value="0"/>
|
||||||
|
<maxContours value="0"/>
|
||||||
|
<maxCompositePoints value="0"/>
|
||||||
|
<maxCompositeContours value="0"/>
|
||||||
|
<maxZones value="2"/>
|
||||||
|
<maxTwilightPoints value="0"/>
|
||||||
|
<maxStorage value="0"/>
|
||||||
|
<maxFunctionDefs value="0"/>
|
||||||
|
<maxInstructionDefs value="0"/>
|
||||||
|
<maxStackElements value="0"/>
|
||||||
|
<maxSizeOfInstructions value="0"/>
|
||||||
|
<maxComponentElements value="0"/>
|
||||||
|
<maxComponentDepth value="0"/>
|
||||||
|
</maxp>
|
||||||
|
|
||||||
|
<post>
|
||||||
|
<formatType value="2.0"/>
|
||||||
|
<italicAngle value="0.0"/>
|
||||||
|
<underlinePosition value="0"/>
|
||||||
|
<underlineThickness value="0"/>
|
||||||
|
<isFixedPitch value="0"/>
|
||||||
|
<minMemType42 value="0"/>
|
||||||
|
<maxMemType42 value="0"/>
|
||||||
|
<minMemType1 value="0"/>
|
||||||
|
<maxMemType1 value="0"/>
|
||||||
|
<psNames>
|
||||||
|
<!-- This file uses unique glyph names based on the information
|
||||||
|
found in the 'post' table. Since these names might not be unique,
|
||||||
|
we have to invent artificial names in case of clashes. In order to
|
||||||
|
be able to retain the original information, we need a name to
|
||||||
|
ps name mapping for those cases where they differ. That's what
|
||||||
|
you see below.
|
||||||
|
-->
|
||||||
|
</psNames>
|
||||||
|
<extraNames>
|
||||||
|
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||||
|
</extraNames>
|
||||||
|
</post>
|
||||||
|
|
||||||
|
<GPOS>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<ScriptList>
|
||||||
|
<!-- ScriptCount=1 -->
|
||||||
|
<ScriptRecord index="0">
|
||||||
|
<ScriptTag value="latn"/>
|
||||||
|
<Script>
|
||||||
|
<DefaultLangSys>
|
||||||
|
<ReqFeatureIndex value="65535"/>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureIndex index="0" value="0"/>
|
||||||
|
</DefaultLangSys>
|
||||||
|
<!-- LangSysCount=0 -->
|
||||||
|
</Script>
|
||||||
|
</ScriptRecord>
|
||||||
|
</ScriptList>
|
||||||
|
<FeatureList>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureRecord index="0">
|
||||||
|
<FeatureTag value="test"/>
|
||||||
|
<Feature>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<LookupListIndex index="0" value="0"/>
|
||||||
|
</Feature>
|
||||||
|
</FeatureRecord>
|
||||||
|
</FeatureList>
|
||||||
|
<LookupList>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<Lookup index="0">
|
||||||
|
<LookupType value="9"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<ExtensionPos index="0" Format="1">
|
||||||
|
<ExtensionLookupType value="1"/>
|
||||||
|
<SinglePos Format="1">
|
||||||
|
<Coverage Format="1">
|
||||||
|
<Glyph value="A"/>
|
||||||
|
</Coverage>
|
||||||
|
<ValueFormat value="4"/>
|
||||||
|
<Value XAdvance="20"/>
|
||||||
|
</SinglePos>
|
||||||
|
</ExtensionPos>
|
||||||
|
</Lookup>
|
||||||
|
</LookupList>
|
||||||
|
</GPOS>
|
||||||
|
|
||||||
|
</ttFont>
|
146
Tests/otlLib/data/gsub_51.ttx
Normal file
146
Tests/otlLib/data/gsub_51.ttx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
|
||||||
|
|
||||||
|
<GlyphOrder>
|
||||||
|
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||||
|
<GlyphID id="0" name=".notdef"/>
|
||||||
|
<GlyphID id="1" name="g20"/>
|
||||||
|
<GlyphID id="2" name="g21"/>
|
||||||
|
<GlyphID id="3" name="g22"/>
|
||||||
|
<GlyphID id="4" name="g60"/>
|
||||||
|
<GlyphID id="5" name="g61"/>
|
||||||
|
<GlyphID id="6" name="g62"/>
|
||||||
|
<GlyphID id="7" name="g63"/>
|
||||||
|
</GlyphOrder>
|
||||||
|
|
||||||
|
<maxp>
|
||||||
|
<!-- Most of this table will be recalculated by the compiler -->
|
||||||
|
<tableVersion value="0x10000"/>
|
||||||
|
<numGlyphs value="8"/>
|
||||||
|
<maxPoints value="0"/>
|
||||||
|
<maxContours value="0"/>
|
||||||
|
<maxCompositePoints value="0"/>
|
||||||
|
<maxCompositeContours value="0"/>
|
||||||
|
<maxZones value="2"/>
|
||||||
|
<maxTwilightPoints value="0"/>
|
||||||
|
<maxStorage value="0"/>
|
||||||
|
<maxFunctionDefs value="0"/>
|
||||||
|
<maxInstructionDefs value="0"/>
|
||||||
|
<maxStackElements value="0"/>
|
||||||
|
<maxSizeOfInstructions value="0"/>
|
||||||
|
<maxComponentElements value="0"/>
|
||||||
|
<maxComponentDepth value="0"/>
|
||||||
|
</maxp>
|
||||||
|
|
||||||
|
<post>
|
||||||
|
<formatType value="2.0"/>
|
||||||
|
<italicAngle value="0.0"/>
|
||||||
|
<underlinePosition value="0"/>
|
||||||
|
<underlineThickness value="0"/>
|
||||||
|
<isFixedPitch value="0"/>
|
||||||
|
<minMemType42 value="0"/>
|
||||||
|
<maxMemType42 value="0"/>
|
||||||
|
<minMemType1 value="0"/>
|
||||||
|
<maxMemType1 value="0"/>
|
||||||
|
<psNames>
|
||||||
|
<!-- This file uses unique glyph names based on the information
|
||||||
|
found in the 'post' table. Since these names might not be unique,
|
||||||
|
we have to invent artificial names in case of clashes. In order to
|
||||||
|
be able to retain the original information, we need a name to
|
||||||
|
ps name mapping for those cases where they differ. That's what
|
||||||
|
you see below.
|
||||||
|
-->
|
||||||
|
</psNames>
|
||||||
|
<extraNames>
|
||||||
|
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||||
|
</extraNames>
|
||||||
|
</post>
|
||||||
|
|
||||||
|
<GSUB>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<ScriptList>
|
||||||
|
<!-- ScriptCount=1 -->
|
||||||
|
<ScriptRecord index="0">
|
||||||
|
<ScriptTag value="latn"/>
|
||||||
|
<Script>
|
||||||
|
<DefaultLangSys>
|
||||||
|
<ReqFeatureIndex value="65535"/>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureIndex index="0" value="0"/>
|
||||||
|
</DefaultLangSys>
|
||||||
|
<!-- LangSysCount=0 -->
|
||||||
|
</Script>
|
||||||
|
</ScriptRecord>
|
||||||
|
</ScriptList>
|
||||||
|
<FeatureList>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureRecord index="0">
|
||||||
|
<FeatureTag value="test"/>
|
||||||
|
<Feature>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<LookupListIndex index="0" value="4"/>
|
||||||
|
</Feature>
|
||||||
|
</FeatureRecord>
|
||||||
|
</FeatureList>
|
||||||
|
<LookupList>
|
||||||
|
<!-- LookupCount=5 -->
|
||||||
|
<Lookup index="0">
|
||||||
|
<LookupType value="1"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<SingleSubst index="0" Format="1">
|
||||||
|
<Substitution in="g20" out="g60"/>
|
||||||
|
<Substitution in="g21" out="g61"/>
|
||||||
|
</SingleSubst>
|
||||||
|
</Lookup>
|
||||||
|
<Lookup index="1">
|
||||||
|
<LookupType value="4"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<LigatureSubst index="0" Format="1">
|
||||||
|
<LigatureSet glyph="g21">
|
||||||
|
<Ligature components="g22" glyph="g61"/>
|
||||||
|
</LigatureSet>
|
||||||
|
</LigatureSubst>
|
||||||
|
</Lookup>
|
||||||
|
<Lookup index="2">
|
||||||
|
<LookupType value="4"/>
|
||||||
|
<LookupFlag value="8"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<LigatureSubst index="0" Format="1">
|
||||||
|
<LigatureSet glyph="g21">
|
||||||
|
<Ligature components="g22" glyph="g61"/>
|
||||||
|
</LigatureSet>
|
||||||
|
</LigatureSubst>
|
||||||
|
</Lookup>
|
||||||
|
<Lookup index="3">
|
||||||
|
<LookupType value="2"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<MultipleSubst index="0" Format="1">
|
||||||
|
<Substitution in="g21" out="g61,g62,g63"/>
|
||||||
|
</MultipleSubst>
|
||||||
|
</Lookup>
|
||||||
|
<Lookup index="4">
|
||||||
|
<LookupType value="5"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<ContextSubst index="0" Format="1">
|
||||||
|
<Coverage Format="1">
|
||||||
|
<Glyph value="g20"/>
|
||||||
|
</Coverage>
|
||||||
|
<!-- SubRuleSetCount=1 -->
|
||||||
|
<SubRuleSet index="0">
|
||||||
|
<!-- SubRuleCount=1 -->
|
||||||
|
<SubRule index="0">
|
||||||
|
<!-- GlyphCount=2 -->
|
||||||
|
<!-- SubstCount=0 -->
|
||||||
|
<Input index="0" value="g20"/>
|
||||||
|
</SubRule>
|
||||||
|
</SubRuleSet>
|
||||||
|
</ContextSubst>
|
||||||
|
</Lookup>
|
||||||
|
</LookupList>
|
||||||
|
</GSUB>
|
||||||
|
|
||||||
|
</ttFont>
|
150
Tests/otlLib/data/gsub_52.ttx
Normal file
150
Tests/otlLib/data/gsub_52.ttx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
|
||||||
|
|
||||||
|
<GlyphOrder>
|
||||||
|
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||||
|
<GlyphID id="0" name=".notdef"/>
|
||||||
|
<GlyphID id="1" name="g20"/>
|
||||||
|
<GlyphID id="2" name="g21"/>
|
||||||
|
<GlyphID id="3" name="g22"/>
|
||||||
|
<GlyphID id="4" name="g60"/>
|
||||||
|
<GlyphID id="5" name="g61"/>
|
||||||
|
<GlyphID id="6" name="g62"/>
|
||||||
|
<GlyphID id="7" name="g63"/>
|
||||||
|
</GlyphOrder>
|
||||||
|
|
||||||
|
<maxp>
|
||||||
|
<!-- Most of this table will be recalculated by the compiler -->
|
||||||
|
<tableVersion value="0x10000"/>
|
||||||
|
<numGlyphs value="8"/>
|
||||||
|
<maxPoints value="0"/>
|
||||||
|
<maxContours value="0"/>
|
||||||
|
<maxCompositePoints value="0"/>
|
||||||
|
<maxCompositeContours value="0"/>
|
||||||
|
<maxZones value="2"/>
|
||||||
|
<maxTwilightPoints value="0"/>
|
||||||
|
<maxStorage value="0"/>
|
||||||
|
<maxFunctionDefs value="0"/>
|
||||||
|
<maxInstructionDefs value="0"/>
|
||||||
|
<maxStackElements value="0"/>
|
||||||
|
<maxSizeOfInstructions value="0"/>
|
||||||
|
<maxComponentElements value="0"/>
|
||||||
|
<maxComponentDepth value="0"/>
|
||||||
|
</maxp>
|
||||||
|
|
||||||
|
<post>
|
||||||
|
<formatType value="2.0"/>
|
||||||
|
<italicAngle value="0.0"/>
|
||||||
|
<underlinePosition value="0"/>
|
||||||
|
<underlineThickness value="0"/>
|
||||||
|
<isFixedPitch value="0"/>
|
||||||
|
<minMemType42 value="0"/>
|
||||||
|
<maxMemType42 value="0"/>
|
||||||
|
<minMemType1 value="0"/>
|
||||||
|
<maxMemType1 value="0"/>
|
||||||
|
<psNames>
|
||||||
|
<!-- This file uses unique glyph names based on the information
|
||||||
|
found in the 'post' table. Since these names might not be unique,
|
||||||
|
we have to invent artificial names in case of clashes. In order to
|
||||||
|
be able to retain the original information, we need a name to
|
||||||
|
ps name mapping for those cases where they differ. That's what
|
||||||
|
you see below.
|
||||||
|
-->
|
||||||
|
</psNames>
|
||||||
|
<extraNames>
|
||||||
|
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||||
|
</extraNames>
|
||||||
|
</post>
|
||||||
|
|
||||||
|
<GSUB>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<ScriptList>
|
||||||
|
<!-- ScriptCount=1 -->
|
||||||
|
<ScriptRecord index="0">
|
||||||
|
<ScriptTag value="latn"/>
|
||||||
|
<Script>
|
||||||
|
<DefaultLangSys>
|
||||||
|
<ReqFeatureIndex value="65535"/>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureIndex index="0" value="0"/>
|
||||||
|
</DefaultLangSys>
|
||||||
|
<!-- LangSysCount=0 -->
|
||||||
|
</Script>
|
||||||
|
</ScriptRecord>
|
||||||
|
</ScriptList>
|
||||||
|
<FeatureList>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureRecord index="0">
|
||||||
|
<FeatureTag value="test"/>
|
||||||
|
<Feature>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<LookupListIndex index="0" value="4"/>
|
||||||
|
</Feature>
|
||||||
|
</FeatureRecord>
|
||||||
|
</FeatureList>
|
||||||
|
<LookupList>
|
||||||
|
<!-- LookupCount=5 -->
|
||||||
|
<Lookup index="0">
|
||||||
|
<LookupType value="1"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<SingleSubst index="0" Format="1">
|
||||||
|
<Substitution in="g20" out="g60"/>
|
||||||
|
<Substitution in="g21" out="g61"/>
|
||||||
|
</SingleSubst>
|
||||||
|
</Lookup>
|
||||||
|
<Lookup index="1">
|
||||||
|
<LookupType value="4"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<LigatureSubst index="0" Format="1">
|
||||||
|
<LigatureSet glyph="g21">
|
||||||
|
<Ligature components="g22" glyph="g61"/>
|
||||||
|
</LigatureSet>
|
||||||
|
</LigatureSubst>
|
||||||
|
</Lookup>
|
||||||
|
<Lookup index="2">
|
||||||
|
<LookupType value="4"/>
|
||||||
|
<LookupFlag value="8"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<LigatureSubst index="0" Format="1">
|
||||||
|
<LigatureSet glyph="g21">
|
||||||
|
<Ligature components="g22" glyph="g61"/>
|
||||||
|
</LigatureSet>
|
||||||
|
</LigatureSubst>
|
||||||
|
</Lookup>
|
||||||
|
<Lookup index="3">
|
||||||
|
<LookupType value="2"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<MultipleSubst index="0" Format="1">
|
||||||
|
<Substitution in="g21" out="g61,g62,g63"/>
|
||||||
|
</MultipleSubst>
|
||||||
|
</Lookup>
|
||||||
|
<Lookup index="4">
|
||||||
|
<LookupType value="5"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<ContextSubst index="0" Format="2">
|
||||||
|
<Coverage Format="1">
|
||||||
|
<Glyph value="g20"/>
|
||||||
|
</Coverage>
|
||||||
|
<ClassDef Format="2">
|
||||||
|
<ClassDef glyph="g20" class="1"/>
|
||||||
|
</ClassDef>
|
||||||
|
<!-- SubClassSetCount=2 -->
|
||||||
|
<SubClassSet index="0" empty="1"/>
|
||||||
|
<SubClassSet index="1">
|
||||||
|
<!-- SubClassRuleCount=1 -->
|
||||||
|
<SubClassRule index="0">
|
||||||
|
<!-- GlyphCount=2 -->
|
||||||
|
<!-- SubstCount=0 -->
|
||||||
|
<Class index="0" value="1"/>
|
||||||
|
</SubClassRule>
|
||||||
|
</SubClassSet>
|
||||||
|
</ContextSubst>
|
||||||
|
</Lookup>
|
||||||
|
</LookupList>
|
||||||
|
</GSUB>
|
||||||
|
|
||||||
|
</ttFont>
|
99
Tests/otlLib/data/gsub_71.ttx
Normal file
99
Tests/otlLib/data/gsub_71.ttx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
|
||||||
|
|
||||||
|
<GlyphOrder>
|
||||||
|
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||||
|
<GlyphID id="0" name=".notdef"/>
|
||||||
|
<GlyphID id="1" name="g18"/>
|
||||||
|
<GlyphID id="2" name="g19"/>
|
||||||
|
<GlyphID id="3" name="g23"/>
|
||||||
|
<GlyphID id="4" name="g24"/>
|
||||||
|
</GlyphOrder>
|
||||||
|
|
||||||
|
<maxp>
|
||||||
|
<!-- Most of this table will be recalculated by the compiler -->
|
||||||
|
<tableVersion value="0x10000"/>
|
||||||
|
<numGlyphs value="5"/>
|
||||||
|
<maxPoints value="0"/>
|
||||||
|
<maxContours value="0"/>
|
||||||
|
<maxCompositePoints value="0"/>
|
||||||
|
<maxCompositeContours value="0"/>
|
||||||
|
<maxZones value="2"/>
|
||||||
|
<maxTwilightPoints value="0"/>
|
||||||
|
<maxStorage value="0"/>
|
||||||
|
<maxFunctionDefs value="0"/>
|
||||||
|
<maxInstructionDefs value="0"/>
|
||||||
|
<maxStackElements value="0"/>
|
||||||
|
<maxSizeOfInstructions value="0"/>
|
||||||
|
<maxComponentElements value="0"/>
|
||||||
|
<maxComponentDepth value="0"/>
|
||||||
|
</maxp>
|
||||||
|
|
||||||
|
<post>
|
||||||
|
<formatType value="2.0"/>
|
||||||
|
<italicAngle value="0.0"/>
|
||||||
|
<underlinePosition value="0"/>
|
||||||
|
<underlineThickness value="0"/>
|
||||||
|
<isFixedPitch value="0"/>
|
||||||
|
<minMemType42 value="0"/>
|
||||||
|
<maxMemType42 value="0"/>
|
||||||
|
<minMemType1 value="0"/>
|
||||||
|
<maxMemType1 value="0"/>
|
||||||
|
<psNames>
|
||||||
|
<!-- This file uses unique glyph names based on the information
|
||||||
|
found in the 'post' table. Since these names might not be unique,
|
||||||
|
we have to invent artificial names in case of clashes. In order to
|
||||||
|
be able to retain the original information, we need a name to
|
||||||
|
ps name mapping for those cases where they differ. That's what
|
||||||
|
you see below.
|
||||||
|
-->
|
||||||
|
</psNames>
|
||||||
|
<extraNames>
|
||||||
|
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||||
|
</extraNames>
|
||||||
|
</post>
|
||||||
|
|
||||||
|
<GSUB>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<ScriptList>
|
||||||
|
<!-- ScriptCount=1 -->
|
||||||
|
<ScriptRecord index="0">
|
||||||
|
<ScriptTag value="latn"/>
|
||||||
|
<Script>
|
||||||
|
<DefaultLangSys>
|
||||||
|
<ReqFeatureIndex value="65535"/>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureIndex index="0" value="0"/>
|
||||||
|
</DefaultLangSys>
|
||||||
|
<!-- LangSysCount=0 -->
|
||||||
|
</Script>
|
||||||
|
</ScriptRecord>
|
||||||
|
</ScriptList>
|
||||||
|
<FeatureList>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureRecord index="0">
|
||||||
|
<FeatureTag value="test"/>
|
||||||
|
<Feature>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<LookupListIndex index="0" value="0"/>
|
||||||
|
</Feature>
|
||||||
|
</FeatureRecord>
|
||||||
|
</FeatureList>
|
||||||
|
<LookupList>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<Lookup index="0">
|
||||||
|
<LookupType value="7"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<ExtensionSubst index="0" Format="1">
|
||||||
|
<ExtensionLookupType value="1"/>
|
||||||
|
<SingleSubst Format="1">
|
||||||
|
<Substitution in="g18" out="g23"/>
|
||||||
|
<Substitution in="g19" out="g24"/>
|
||||||
|
</SingleSubst>
|
||||||
|
</ExtensionSubst>
|
||||||
|
</Lookup>
|
||||||
|
</LookupList>
|
||||||
|
</GSUB>
|
||||||
|
|
||||||
|
</ttFont>
|
76
Tests/otlLib/maxContextCalc_test.py
Normal file
76
Tests/otlLib/maxContextCalc_test.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
from fontTools.otlLib.maxContextCalc import maxCtxFont
|
||||||
|
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_ctx_calc_no_features():
|
||||||
|
font = TTFont()
|
||||||
|
assert maxCtxFont(font) == 0
|
||||||
|
font.setGlyphOrder(['.notdef'])
|
||||||
|
addOpenTypeFeaturesFromString(font, '')
|
||||||
|
assert maxCtxFont(font) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_ctx_calc_features():
|
||||||
|
glyphs = '.notdef space A B C a b c'.split()
|
||||||
|
features = """
|
||||||
|
lookup GSUB_EXT useExtension {
|
||||||
|
sub a by b;
|
||||||
|
} GSUB_EXT;
|
||||||
|
|
||||||
|
lookup GPOS_EXT useExtension {
|
||||||
|
pos a b -10;
|
||||||
|
} GPOS_EXT;
|
||||||
|
|
||||||
|
feature sub1 {
|
||||||
|
sub A by a;
|
||||||
|
sub A B by b;
|
||||||
|
sub A B C by c;
|
||||||
|
sub [A B] C by c;
|
||||||
|
sub [A B] C [A B] by c;
|
||||||
|
sub A by A B;
|
||||||
|
sub A' C by A B;
|
||||||
|
sub a' by b;
|
||||||
|
sub a' b by c;
|
||||||
|
sub a from [A B C];
|
||||||
|
rsub a by b;
|
||||||
|
rsub a' by b;
|
||||||
|
rsub a b' by c;
|
||||||
|
rsub a b' c by A;
|
||||||
|
rsub [a b] c' by A;
|
||||||
|
rsub [a b] c' [a b] by B;
|
||||||
|
lookup GSUB_EXT;
|
||||||
|
} sub1;
|
||||||
|
|
||||||
|
feature pos1 {
|
||||||
|
pos A 20;
|
||||||
|
pos A B -50;
|
||||||
|
pos A B' 10 C;
|
||||||
|
lookup GPOS_EXT;
|
||||||
|
} pos1;
|
||||||
|
"""
|
||||||
|
font = TTFont()
|
||||||
|
font.setGlyphOrder(glyphs)
|
||||||
|
addOpenTypeFeaturesFromString(font, features)
|
||||||
|
|
||||||
|
assert maxCtxFont(font) == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('file_name, max_context', [
|
||||||
|
('gsub_51', 2),
|
||||||
|
('gsub_52', 2),
|
||||||
|
('gsub_71', 1),
|
||||||
|
('gpos_91', 1),
|
||||||
|
])
|
||||||
|
def test_max_ctx_calc_features_ttx(file_name, max_context):
|
||||||
|
ttx_path = os.path.join(os.path.dirname(__file__),
|
||||||
|
'data', '{}.ttx'.format(file_name))
|
||||||
|
font = TTFont()
|
||||||
|
font.importXML(ttx_path)
|
||||||
|
|
||||||
|
assert maxCtxFont(font) == max_context
|
443
Tests/subset/data/TestHVVAR.ttx
Normal file
443
Tests/subset/data/TestHVVAR.ttx
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
|
||||||
|
|
||||||
|
<GlyphOrder>
|
||||||
|
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||||
|
<GlyphID id="0" name=".notdef"/>
|
||||||
|
<GlyphID id="1" name="A"/>
|
||||||
|
<GlyphID id="2" name="B"/>
|
||||||
|
<GlyphID id="3" name="C"/>
|
||||||
|
<GlyphID id="4" name="D"/>
|
||||||
|
<GlyphID id="5" name="E"/>
|
||||||
|
</GlyphOrder>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!-- Most of this table will be recalculated by the compiler -->
|
||||||
|
<tableVersion value="1.0"/>
|
||||||
|
<fontRevision value="1.0"/>
|
||||||
|
<checkSumAdjustment value="0xddab44f"/>
|
||||||
|
<magicNumber value="0x5f0f3cf5"/>
|
||||||
|
<flags value="00000000 00000001"/>
|
||||||
|
<unitsPerEm value="1000"/>
|
||||||
|
<created value="Tue Apr 23 17:22:52 2019"/>
|
||||||
|
<modified value="Tue Apr 23 17:22:52 2019"/>
|
||||||
|
<xMin value="0"/>
|
||||||
|
<yMin value="0"/>
|
||||||
|
<xMax value="0"/>
|
||||||
|
<yMax value="0"/>
|
||||||
|
<macStyle value="00000000 00000000"/>
|
||||||
|
<lowestRecPPEM value="7"/>
|
||||||
|
<fontDirectionHint value="2"/>
|
||||||
|
<indexToLocFormat value="0"/>
|
||||||
|
<glyphDataFormat value="0"/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<hhea>
|
||||||
|
<tableVersion value="0x00010000"/>
|
||||||
|
<ascent value="1000"/>
|
||||||
|
<descent value="-200"/>
|
||||||
|
<lineGap value="0"/>
|
||||||
|
<advanceWidthMax value="700"/>
|
||||||
|
<minLeftSideBearing value="40"/>
|
||||||
|
<minRightSideBearing value="580"/>
|
||||||
|
<xMaxExtent value="80"/>
|
||||||
|
<caretSlopeRise value="1"/>
|
||||||
|
<caretSlopeRun value="0"/>
|
||||||
|
<caretOffset value="0"/>
|
||||||
|
<reserved0 value="0"/>
|
||||||
|
<reserved1 value="0"/>
|
||||||
|
<reserved2 value="0"/>
|
||||||
|
<reserved3 value="0"/>
|
||||||
|
<metricDataFormat value="0"/>
|
||||||
|
<numberOfHMetrics value="4"/>
|
||||||
|
</hhea>
|
||||||
|
|
||||||
|
<maxp>
|
||||||
|
<!-- Most of this table will be recalculated by the compiler -->
|
||||||
|
<tableVersion value="0x10000"/>
|
||||||
|
<numGlyphs value="6"/>
|
||||||
|
<maxPoints value="1"/>
|
||||||
|
<maxContours value="1"/>
|
||||||
|
<maxCompositePoints value="0"/>
|
||||||
|
<maxCompositeContours value="0"/>
|
||||||
|
<maxZones value="1"/>
|
||||||
|
<maxTwilightPoints value="0"/>
|
||||||
|
<maxStorage value="0"/>
|
||||||
|
<maxFunctionDefs value="1"/>
|
||||||
|
<maxInstructionDefs value="0"/>
|
||||||
|
<maxStackElements value="0"/>
|
||||||
|
<maxSizeOfInstructions value="0"/>
|
||||||
|
<maxComponentElements value="0"/>
|
||||||
|
<maxComponentDepth value="0"/>
|
||||||
|
</maxp>
|
||||||
|
|
||||||
|
<OS_2>
|
||||||
|
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
|
||||||
|
will be recalculated by the compiler -->
|
||||||
|
<version value="3"/>
|
||||||
|
<xAvgCharWidth value="606"/>
|
||||||
|
<usWeightClass value="400"/>
|
||||||
|
<usWidthClass value="5"/>
|
||||||
|
<fsType value="00000000 00001000"/>
|
||||||
|
<ySubscriptXSize value="650"/>
|
||||||
|
<ySubscriptYSize value="600"/>
|
||||||
|
<ySubscriptXOffset value="0"/>
|
||||||
|
<ySubscriptYOffset value="75"/>
|
||||||
|
<ySuperscriptXSize value="650"/>
|
||||||
|
<ySuperscriptYSize value="600"/>
|
||||||
|
<ySuperscriptXOffset value="0"/>
|
||||||
|
<ySuperscriptYOffset value="350"/>
|
||||||
|
<yStrikeoutSize value="50"/>
|
||||||
|
<yStrikeoutPosition value="300"/>
|
||||||
|
<sFamilyClass value="0"/>
|
||||||
|
<panose>
|
||||||
|
<bFamilyType value="0"/>
|
||||||
|
<bSerifStyle value="0"/>
|
||||||
|
<bWeight value="5"/>
|
||||||
|
<bProportion value="0"/>
|
||||||
|
<bContrast value="0"/>
|
||||||
|
<bStrokeVariation value="0"/>
|
||||||
|
<bArmStyle value="0"/>
|
||||||
|
<bLetterForm value="0"/>
|
||||||
|
<bMidline value="0"/>
|
||||||
|
<bXHeight value="0"/>
|
||||||
|
</panose>
|
||||||
|
<ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
|
||||||
|
<ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
|
||||||
|
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
|
||||||
|
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
|
||||||
|
<achVendID value="UKWN"/>
|
||||||
|
<fsSelection value="00000000 01000000"/>
|
||||||
|
<usFirstCharIndex value="32"/>
|
||||||
|
<usLastCharIndex value="8722"/>
|
||||||
|
<sTypoAscender value="800"/>
|
||||||
|
<sTypoDescender value="-200"/>
|
||||||
|
<sTypoLineGap value="200"/>
|
||||||
|
<usWinAscent value="1000"/>
|
||||||
|
<usWinDescent value="200"/>
|
||||||
|
<ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
|
||||||
|
<ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
|
||||||
|
<sxHeight value="500"/>
|
||||||
|
<sCapHeight value="700"/>
|
||||||
|
<usDefaultChar value="0"/>
|
||||||
|
<usBreakChar value="32"/>
|
||||||
|
<usMaxContext value="0"/>
|
||||||
|
</OS_2>
|
||||||
|
|
||||||
|
<hmtx>
|
||||||
|
<mtx name=".notdef" width="700" lsb="80"/>
|
||||||
|
<mtx name="A" width="700" lsb="0"/>
|
||||||
|
<mtx name="B" width="625" lsb="40"/>
|
||||||
|
<mtx name="C" width="660" lsb="80"/>
|
||||||
|
<mtx name="D" width="660" lsb="80"/>
|
||||||
|
<mtx name="E" width="660" lsb="80"/>
|
||||||
|
</hmtx>
|
||||||
|
|
||||||
|
<cmap>
|
||||||
|
<tableVersion version="0"/>
|
||||||
|
<cmap_format_4 platformID="0" platEncID="3" language="0">
|
||||||
|
<map code="0x41" name="A"/><!-- SPACE -->
|
||||||
|
<map code="0x42" name="B"/><!-- DIGIT ZERO -->
|
||||||
|
<map code="0x43" name="C"/><!-- EQUALS SIGN -->
|
||||||
|
<map code="0x44" name="D"/><!-- MINUS SIGN -->
|
||||||
|
<map code="0x45" name="E"/><!-- PLUS SIGN -->
|
||||||
|
</cmap_format_4>
|
||||||
|
</cmap>
|
||||||
|
|
||||||
|
<loca>
|
||||||
|
<!-- The 'loca' table will be calculated by the compiler -->
|
||||||
|
</loca>
|
||||||
|
|
||||||
|
<glyf>
|
||||||
|
|
||||||
|
<!-- The xMin, yMin, xMax and yMax values
|
||||||
|
will be recalculated by the compiler. -->
|
||||||
|
|
||||||
|
<TTGlyph name=".notdef" xMin="0" yMin="0" xMax="0" yMax="0">
|
||||||
|
<contour>
|
||||||
|
<pt x="0" y="0" on="1"/>
|
||||||
|
</contour>
|
||||||
|
<instructions/>
|
||||||
|
</TTGlyph>
|
||||||
|
|
||||||
|
<TTGlyph name="A"/><!-- contains no outline data -->
|
||||||
|
|
||||||
|
<TTGlyph name="B" xMin="0" yMin="0" xMax="0" yMax="0">
|
||||||
|
<contour>
|
||||||
|
<pt x="0" y="0" on="1"/>
|
||||||
|
</contour>
|
||||||
|
<instructions/>
|
||||||
|
</TTGlyph>
|
||||||
|
|
||||||
|
<TTGlyph name="C" xMin="0" yMin="0" xMax="0" yMax="0">
|
||||||
|
<contour>
|
||||||
|
<pt x="0" y="0" on="1"/>
|
||||||
|
</contour>
|
||||||
|
<instructions/>
|
||||||
|
</TTGlyph>
|
||||||
|
|
||||||
|
<TTGlyph name="D" xMin="0" yMin="0" xMax="0" yMax="0">
|
||||||
|
<contour>
|
||||||
|
<pt x="0" y="0" on="1"/>
|
||||||
|
</contour>
|
||||||
|
<instructions/>
|
||||||
|
</TTGlyph>
|
||||||
|
|
||||||
|
<TTGlyph name="E" xMin="0" yMin="0" xMax="0" yMax="0">
|
||||||
|
<contour>
|
||||||
|
<pt x="0" y="0" on="1"/>
|
||||||
|
</contour>
|
||||||
|
<instructions/>
|
||||||
|
</TTGlyph>
|
||||||
|
|
||||||
|
</glyf>
|
||||||
|
|
||||||
|
<name>
|
||||||
|
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||||
|
TestHVVAR
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||||
|
Regular
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="3" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||||
|
1.000;UKWN;TestHVVAR-Regular
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="4" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||||
|
TestHVVAR
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="5" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||||
|
Version 1.000
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||||
|
TestHVVAR-Regular
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
|
||||||
|
TestHVVAR
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
|
||||||
|
Regular
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
|
||||||
|
1.000;UKWN;TestHVVAR-Regular
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
|
||||||
|
TestHVVAR-Regular
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
|
||||||
|
Version 1.000
|
||||||
|
</namerecord>
|
||||||
|
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
|
||||||
|
TestHVVAR-Regular
|
||||||
|
</namerecord>
|
||||||
|
</name>
|
||||||
|
|
||||||
|
<post>
|
||||||
|
<formatType value="2.0"/>
|
||||||
|
<italicAngle value="0.0"/>
|
||||||
|
<underlinePosition value="-75"/>
|
||||||
|
<underlineThickness value="50"/>
|
||||||
|
<isFixedPitch value="0"/>
|
||||||
|
<minMemType42 value="0"/>
|
||||||
|
<maxMemType42 value="0"/>
|
||||||
|
<minMemType1 value="0"/>
|
||||||
|
<maxMemType1 value="0"/>
|
||||||
|
<psNames>
|
||||||
|
<!-- This file uses unique glyph names based on the information
|
||||||
|
found in the 'post' table. Since these names might not be unique,
|
||||||
|
we have to invent artificial names in case of clashes. In order to
|
||||||
|
be able to retain the original information, we need a name to
|
||||||
|
ps name mapping for those cases where they differ. That's what
|
||||||
|
you see below.
|
||||||
|
-->
|
||||||
|
</psNames>
|
||||||
|
<extraNames>
|
||||||
|
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||||
|
</extraNames>
|
||||||
|
</post>
|
||||||
|
|
||||||
|
<HVAR>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<VarStore Format="1">
|
||||||
|
<Format value="1"/>
|
||||||
|
<VarRegionList>
|
||||||
|
<!-- RegionAxisCount=1 -->
|
||||||
|
<!-- RegionCount=2 -->
|
||||||
|
<Region index="0">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="-1.0"/>
|
||||||
|
<PeakCoord value="-1.0"/>
|
||||||
|
<EndCoord value="0.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="1">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="1.0"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
</VarRegionList>
|
||||||
|
<!-- VarDataCount=2 -->
|
||||||
|
<VarData index="0">
|
||||||
|
<!-- ItemCount=6 -->
|
||||||
|
<NumShorts value="0"/>
|
||||||
|
<!-- VarRegionCount=1 -->
|
||||||
|
<VarRegionIndex index="0" value="1"/>
|
||||||
|
<Item index="0" value="[1]"/>
|
||||||
|
<Item index="1" value="[2]"/>
|
||||||
|
<Item index="2" value="[3]"/>
|
||||||
|
<Item index="3" value="[4]"/>
|
||||||
|
<Item index="4" value="[5]"/>
|
||||||
|
<Item index="5" value="[6]"/>
|
||||||
|
</VarData>
|
||||||
|
<VarData index="1">
|
||||||
|
<!-- ItemCount=6 -->
|
||||||
|
<NumShorts value="1"/>
|
||||||
|
<!-- VarRegionCount=2 -->
|
||||||
|
<VarRegionIndex index="0" value="0"/>
|
||||||
|
<VarRegionIndex index="1" value="1"/>
|
||||||
|
<Item index="0" value="[10, 21]"/>
|
||||||
|
<Item index="1" value="[11, 22]"/>
|
||||||
|
<Item index="2" value="[126, 23]"/>
|
||||||
|
<Item index="3" value="[127, 24]"/>
|
||||||
|
<Item index="4" value="[-128, 25]"/>
|
||||||
|
<Item index="5" value="[-129, 26]"/>
|
||||||
|
</VarData>
|
||||||
|
</VarStore>
|
||||||
|
<LsbMap>
|
||||||
|
<Map glyph=".notdef" outer="0" inner="2"/>
|
||||||
|
<Map glyph="A" outer="0" inner="5"/>
|
||||||
|
<Map glyph="B" outer="1" inner="1"/>
|
||||||
|
<Map glyph="C" outer="1" inner="4"/>
|
||||||
|
<Map glyph="D" outer="0" inner="3"/>
|
||||||
|
<Map glyph="E" outer="1" inner="0"/>
|
||||||
|
</LsbMap>
|
||||||
|
<RsbMap>
|
||||||
|
<Map glyph=".notdef" outer="0" inner="1"/>
|
||||||
|
<Map glyph="A" outer="0" inner="4"/>
|
||||||
|
<Map glyph="B" outer="1" inner="5"/>
|
||||||
|
<Map glyph="C" outer="0" inner="0"/>
|
||||||
|
<Map glyph="D" outer="1" inner="2"/>
|
||||||
|
<Map glyph="E" outer="1" inner="3"/>
|
||||||
|
</RsbMap>
|
||||||
|
</HVAR>
|
||||||
|
|
||||||
|
<VVAR>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<VarStore Format="1">
|
||||||
|
<Format value="1"/>
|
||||||
|
<VarRegionList>
|
||||||
|
<!-- RegionAxisCount=1 -->
|
||||||
|
<!-- RegionCount=4 -->
|
||||||
|
<Region index="0">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="-1.0"/>
|
||||||
|
<PeakCoord value="-1.0"/>
|
||||||
|
<EndCoord value="0.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="1">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="1.0"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="2">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="-1.0"/>
|
||||||
|
<PeakCoord value="-0.5"/>
|
||||||
|
<EndCoord value="0.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="3">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="0.5"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
</VarRegionList>
|
||||||
|
<!-- VarDataCount=2 -->
|
||||||
|
<VarData index="0">
|
||||||
|
<!-- ItemCount=4 -->
|
||||||
|
<NumShorts value="0"/>
|
||||||
|
<!-- VarRegionCount=1 -->
|
||||||
|
<VarRegionIndex index="0" value="1"/>
|
||||||
|
<Item index="0" value="[32]"/>
|
||||||
|
<Item index="1" value="[17]"/>
|
||||||
|
<Item index="2" value="[0]"/>
|
||||||
|
<Item index="3" value="[14]"/>
|
||||||
|
<Item index="4" value="[5]"/>
|
||||||
|
<Item index="5" value="[6]"/>
|
||||||
|
</VarData>
|
||||||
|
<VarData index="1">
|
||||||
|
<!-- ItemCount=6 -->
|
||||||
|
<NumShorts value="3"/>
|
||||||
|
<!-- VarRegionCount=4 -->
|
||||||
|
<VarRegionIndex index="0" value="0"/>
|
||||||
|
<VarRegionIndex index="1" value="1"/>
|
||||||
|
<VarRegionIndex index="2" value="2"/>
|
||||||
|
<VarRegionIndex index="3" value="3"/>
|
||||||
|
<Item index="0" value="[0, 1, 11, 21]"/>
|
||||||
|
<Item index="1" value="[0, 2, 12, 22]"/>
|
||||||
|
<Item index="2" value="[128, 130, 13, 23]"/>
|
||||||
|
<Item index="3" value="[0, 4, 14, 24]"/>
|
||||||
|
<Item index="4" value="[0, 5, -129, 25]"/>
|
||||||
|
<Item index="5" value="[14, 6, 16, 26]"/>
|
||||||
|
</VarData>
|
||||||
|
</VarStore>
|
||||||
|
<AdvHeightMap>
|
||||||
|
<Map glyph=".notdef" outer="1" inner="0"/>
|
||||||
|
<Map glyph="A" outer="1" inner="2"/>
|
||||||
|
<Map glyph="B" outer="0" inner="2"/>
|
||||||
|
<Map glyph="C" outer="0" inner="0"/>
|
||||||
|
<Map glyph="D" outer="1" inner="1"/>
|
||||||
|
<Map glyph="E" outer="0" inner="1"/>
|
||||||
|
</AdvHeightMap>
|
||||||
|
<VOrgMap>
|
||||||
|
<Map glyph=".notdef" outer="1" inner="3"/>
|
||||||
|
<Map glyph="A" outer="1" inner="5"/>
|
||||||
|
<Map glyph="B" outer="0" inner="5"/>
|
||||||
|
<Map glyph="C" outer="0" inner="3"/>
|
||||||
|
<Map glyph="D" outer="1" inner="4"/>
|
||||||
|
<Map glyph="E" outer="0" inner="4"/>
|
||||||
|
</VOrgMap>
|
||||||
|
</VVAR>
|
||||||
|
|
||||||
|
<avar>
|
||||||
|
<segment axis="wght">
|
||||||
|
<mapping from="-1.0" to="-1.0"/>
|
||||||
|
<mapping from="0.0" to="0.0"/>
|
||||||
|
<mapping from="0.3" to="0.5"/>
|
||||||
|
<mapping from="1.0" to="1.0"/>
|
||||||
|
</segment>
|
||||||
|
</avar>
|
||||||
|
|
||||||
|
<fvar>
|
||||||
|
<Axis>
|
||||||
|
<AxisTag>wght</AxisTag>
|
||||||
|
<Flags>0x0</Flags>
|
||||||
|
<MinValue>100.0</MinValue>
|
||||||
|
<DefaultValue>400.0</DefaultValue>
|
||||||
|
<MaxValue>900.0</MaxValue>
|
||||||
|
<AxisNameID>257</AxisNameID>
|
||||||
|
</Axis>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="258">
|
||||||
|
<coord axis="wght" value="100.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="259">
|
||||||
|
<coord axis="wght" value="300.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="260">
|
||||||
|
<coord axis="wght" value="400.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="261">
|
||||||
|
<coord axis="wght" value="700.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="262">
|
||||||
|
<coord axis="wght" value="900.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
</fvar>
|
||||||
|
|
||||||
|
</ttFont>
|
166
Tests/subset/data/expect_HVVAR.ttx
Normal file
166
Tests/subset/data/expect_HVVAR.ttx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
|
||||||
|
|
||||||
|
<GlyphOrder>
|
||||||
|
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||||
|
<GlyphID id="0" name=".notdef"/>
|
||||||
|
<GlyphID id="1" name="B"/>
|
||||||
|
<GlyphID id="2" name="D"/>
|
||||||
|
</GlyphOrder>
|
||||||
|
|
||||||
|
<HVAR>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<VarStore Format="1">
|
||||||
|
<Format value="1"/>
|
||||||
|
<VarRegionList>
|
||||||
|
<!-- RegionAxisCount=1 -->
|
||||||
|
<!-- RegionCount=2 -->
|
||||||
|
<Region index="0">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="-1.0"/>
|
||||||
|
<PeakCoord value="-1.0"/>
|
||||||
|
<EndCoord value="0.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="1">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="1.0"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
</VarRegionList>
|
||||||
|
<!-- VarDataCount=2 -->
|
||||||
|
<VarData index="0">
|
||||||
|
<!-- ItemCount=5 -->
|
||||||
|
<NumShorts value="0"/>
|
||||||
|
<!-- VarRegionCount=1 -->
|
||||||
|
<VarRegionIndex index="0" value="1"/>
|
||||||
|
<Item index="0" value="[1]"/>
|
||||||
|
<Item index="1" value="[3]"/>
|
||||||
|
<Item index="2" value="[5]"/>
|
||||||
|
<Item index="3" value="[2]"/>
|
||||||
|
<Item index="4" value="[4]"/>
|
||||||
|
</VarData>
|
||||||
|
<VarData index="1">
|
||||||
|
<!-- ItemCount=3 -->
|
||||||
|
<NumShorts value="1"/>
|
||||||
|
<!-- VarRegionCount=2 -->
|
||||||
|
<VarRegionIndex index="0" value="0"/>
|
||||||
|
<VarRegionIndex index="1" value="1"/>
|
||||||
|
<Item index="0" value="[11, 22]"/>
|
||||||
|
<Item index="1" value="[126, 23]"/>
|
||||||
|
<Item index="2" value="[-129, 26]"/>
|
||||||
|
</VarData>
|
||||||
|
</VarStore>
|
||||||
|
<LsbMap>
|
||||||
|
<Map glyph=".notdef" outer="0" inner="1"/>
|
||||||
|
<Map glyph="B" outer="1" inner="0"/>
|
||||||
|
<Map glyph="D" outer="0" inner="4"/>
|
||||||
|
</LsbMap>
|
||||||
|
<RsbMap>
|
||||||
|
<Map glyph=".notdef" outer="0" inner="3"/>
|
||||||
|
<Map glyph="B" outer="1" inner="2"/>
|
||||||
|
<Map glyph="D" outer="1" inner="1"/>
|
||||||
|
</RsbMap>
|
||||||
|
</HVAR>
|
||||||
|
|
||||||
|
<VVAR>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<VarStore Format="1">
|
||||||
|
<Format value="1"/>
|
||||||
|
<VarRegionList>
|
||||||
|
<!-- RegionAxisCount=1 -->
|
||||||
|
<!-- RegionCount=3 -->
|
||||||
|
<Region index="0">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="1.0"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="1">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="-1.0"/>
|
||||||
|
<PeakCoord value="-0.5"/>
|
||||||
|
<EndCoord value="0.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="2">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="0.5"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
</VarRegionList>
|
||||||
|
<!-- VarDataCount=2 -->
|
||||||
|
<VarData index="0">
|
||||||
|
<!-- ItemCount=2 -->
|
||||||
|
<NumShorts value="0"/>
|
||||||
|
<!-- VarRegionCount=1 -->
|
||||||
|
<VarRegionIndex index="0" value="0"/>
|
||||||
|
<Item index="0" value="[0]"/>
|
||||||
|
<Item index="1" value="[6]"/>
|
||||||
|
</VarData>
|
||||||
|
<VarData index="1">
|
||||||
|
<!-- ItemCount=4 -->
|
||||||
|
<NumShorts value="1"/>
|
||||||
|
<!-- VarRegionCount=3 -->
|
||||||
|
<VarRegionIndex index="0" value="1"/>
|
||||||
|
<VarRegionIndex index="1" value="0"/>
|
||||||
|
<VarRegionIndex index="2" value="2"/>
|
||||||
|
<Item index="0" value="[11, 1, 21]"/>
|
||||||
|
<Item index="1" value="[12, 2, 22]"/>
|
||||||
|
<Item index="2" value="[14, 4, 24]"/>
|
||||||
|
<Item index="3" value="[-129, 5, 25]"/>
|
||||||
|
</VarData>
|
||||||
|
</VarStore>
|
||||||
|
<AdvHeightMap>
|
||||||
|
<Map glyph=".notdef" outer="1" inner="0"/>
|
||||||
|
<Map glyph="B" outer="0" inner="0"/>
|
||||||
|
<Map glyph="D" outer="1" inner="1"/>
|
||||||
|
</AdvHeightMap>
|
||||||
|
<VOrgMap>
|
||||||
|
<Map glyph=".notdef" outer="1" inner="2"/>
|
||||||
|
<Map glyph="B" outer="0" inner="1"/>
|
||||||
|
<Map glyph="D" outer="1" inner="3"/>
|
||||||
|
</VOrgMap>
|
||||||
|
</VVAR>
|
||||||
|
|
||||||
|
<avar>
|
||||||
|
<segment axis="wght">
|
||||||
|
<mapping from="-1.0" to="-1.0"/>
|
||||||
|
<mapping from="0.0" to="0.0"/>
|
||||||
|
<mapping from="0.3" to="0.5"/>
|
||||||
|
<mapping from="1.0" to="1.0"/>
|
||||||
|
</segment>
|
||||||
|
</avar>
|
||||||
|
|
||||||
|
<fvar>
|
||||||
|
<Axis>
|
||||||
|
<AxisTag>wght</AxisTag>
|
||||||
|
<Flags>0x0</Flags>
|
||||||
|
<MinValue>100.0</MinValue>
|
||||||
|
<DefaultValue>400.0</DefaultValue>
|
||||||
|
<MaxValue>900.0</MaxValue>
|
||||||
|
<AxisNameID>257</AxisNameID>
|
||||||
|
</Axis>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="258">
|
||||||
|
<coord axis="wght" value="100.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="259">
|
||||||
|
<coord axis="wght" value="300.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="260">
|
||||||
|
<coord axis="wght" value="400.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="261">
|
||||||
|
<coord axis="wght" value="700.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="262">
|
||||||
|
<coord axis="wght" value="900.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
</fvar>
|
||||||
|
|
||||||
|
</ttFont>
|
182
Tests/subset/data/expect_HVVAR_retain_gids.ttx
Normal file
182
Tests/subset/data/expect_HVVAR_retain_gids.ttx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.40">
|
||||||
|
|
||||||
|
<GlyphOrder>
|
||||||
|
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||||
|
<GlyphID id="0" name=".notdef"/>
|
||||||
|
<GlyphID id="1" name="glyph00001"/>
|
||||||
|
<GlyphID id="2" name="B"/>
|
||||||
|
<GlyphID id="3" name="glyph00003"/>
|
||||||
|
<GlyphID id="4" name="D"/>
|
||||||
|
<GlyphID id="5" name="glyph00005"/>
|
||||||
|
</GlyphOrder>
|
||||||
|
|
||||||
|
<HVAR>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<VarStore Format="1">
|
||||||
|
<Format value="1"/>
|
||||||
|
<VarRegionList>
|
||||||
|
<!-- RegionAxisCount=1 -->
|
||||||
|
<!-- RegionCount=2 -->
|
||||||
|
<Region index="0">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="-1.0"/>
|
||||||
|
<PeakCoord value="-1.0"/>
|
||||||
|
<EndCoord value="0.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="1">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="1.0"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
</VarRegionList>
|
||||||
|
<!-- VarDataCount=2 -->
|
||||||
|
<VarData index="0">
|
||||||
|
<!-- ItemCount=6 -->
|
||||||
|
<NumShorts value="0"/>
|
||||||
|
<!-- VarRegionCount=1 -->
|
||||||
|
<VarRegionIndex index="0" value="1"/>
|
||||||
|
<Item index="0" value="[1]"/>
|
||||||
|
<Item index="1" value="[2]"/>
|
||||||
|
<Item index="2" value="[3]"/>
|
||||||
|
<Item index="3" value="[4]"/>
|
||||||
|
<Item index="4" value="[5]"/>
|
||||||
|
<Item index="5" value="[0]"/>
|
||||||
|
</VarData>
|
||||||
|
<VarData index="1">
|
||||||
|
<!-- ItemCount=3 -->
|
||||||
|
<NumShorts value="1"/>
|
||||||
|
<!-- VarRegionCount=2 -->
|
||||||
|
<VarRegionIndex index="0" value="0"/>
|
||||||
|
<VarRegionIndex index="1" value="1"/>
|
||||||
|
<Item index="0" value="[11, 22]"/>
|
||||||
|
<Item index="1" value="[126, 23]"/>
|
||||||
|
<Item index="2" value="[-129, 26]"/>
|
||||||
|
</VarData>
|
||||||
|
</VarStore>
|
||||||
|
<LsbMap>
|
||||||
|
<Map glyph=".notdef" outer="0" inner="2"/>
|
||||||
|
<Map glyph="B" outer="1" inner="0"/>
|
||||||
|
<Map glyph="D" outer="0" inner="3"/>
|
||||||
|
<Map glyph="glyph00001" outer="0" inner="0"/>
|
||||||
|
<Map glyph="glyph00003" outer="0" inner="0"/>
|
||||||
|
<Map glyph="glyph00005" outer="0" inner="3"/>
|
||||||
|
</LsbMap>
|
||||||
|
<RsbMap>
|
||||||
|
<Map glyph=".notdef" outer="0" inner="1"/>
|
||||||
|
<Map glyph="B" outer="1" inner="2"/>
|
||||||
|
<Map glyph="D" outer="1" inner="1"/>
|
||||||
|
<Map glyph="glyph00001" outer="0" inner="0"/>
|
||||||
|
<Map glyph="glyph00003" outer="0" inner="0"/>
|
||||||
|
<Map glyph="glyph00005" outer="1" inner="1"/>
|
||||||
|
</RsbMap>
|
||||||
|
</HVAR>
|
||||||
|
|
||||||
|
<VVAR>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<VarStore Format="1">
|
||||||
|
<Format value="1"/>
|
||||||
|
<VarRegionList>
|
||||||
|
<!-- RegionAxisCount=1 -->
|
||||||
|
<!-- RegionCount=3 -->
|
||||||
|
<Region index="0">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="1.0"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="1">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="-1.0"/>
|
||||||
|
<PeakCoord value="-0.5"/>
|
||||||
|
<EndCoord value="0.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
<Region index="2">
|
||||||
|
<VarRegionAxis index="0">
|
||||||
|
<StartCoord value="0.0"/>
|
||||||
|
<PeakCoord value="0.5"/>
|
||||||
|
<EndCoord value="1.0"/>
|
||||||
|
</VarRegionAxis>
|
||||||
|
</Region>
|
||||||
|
</VarRegionList>
|
||||||
|
<!-- VarDataCount=2 -->
|
||||||
|
<VarData index="0">
|
||||||
|
<!-- ItemCount=2 -->
|
||||||
|
<NumShorts value="0"/>
|
||||||
|
<!-- VarRegionCount=1 -->
|
||||||
|
<VarRegionIndex index="0" value="0"/>
|
||||||
|
<Item index="0" value="[0]"/>
|
||||||
|
<Item index="1" value="[6]"/>
|
||||||
|
</VarData>
|
||||||
|
<VarData index="1">
|
||||||
|
<!-- ItemCount=4 -->
|
||||||
|
<NumShorts value="1"/>
|
||||||
|
<!-- VarRegionCount=3 -->
|
||||||
|
<VarRegionIndex index="0" value="1"/>
|
||||||
|
<VarRegionIndex index="1" value="0"/>
|
||||||
|
<VarRegionIndex index="2" value="2"/>
|
||||||
|
<Item index="0" value="[11, 1, 21]"/>
|
||||||
|
<Item index="1" value="[12, 2, 22]"/>
|
||||||
|
<Item index="2" value="[14, 4, 24]"/>
|
||||||
|
<Item index="3" value="[-129, 5, 25]"/>
|
||||||
|
</VarData>
|
||||||
|
</VarStore>
|
||||||
|
<AdvHeightMap>
|
||||||
|
<Map glyph=".notdef" outer="1" inner="0"/>
|
||||||
|
<Map glyph="B" outer="0" inner="0"/>
|
||||||
|
<Map glyph="D" outer="1" inner="1"/>
|
||||||
|
<Map glyph="glyph00001" outer="0" inner="0"/>
|
||||||
|
<Map glyph="glyph00003" outer="0" inner="0"/>
|
||||||
|
<Map glyph="glyph00005" outer="1" inner="1"/>
|
||||||
|
</AdvHeightMap>
|
||||||
|
<VOrgMap>
|
||||||
|
<Map glyph=".notdef" outer="1" inner="2"/>
|
||||||
|
<Map glyph="B" outer="0" inner="1"/>
|
||||||
|
<Map glyph="D" outer="1" inner="3"/>
|
||||||
|
<Map glyph="glyph00001" outer="0" inner="0"/>
|
||||||
|
<Map glyph="glyph00003" outer="0" inner="0"/>
|
||||||
|
<Map glyph="glyph00005" outer="1" inner="3"/>
|
||||||
|
</VOrgMap>
|
||||||
|
</VVAR>
|
||||||
|
|
||||||
|
<avar>
|
||||||
|
<segment axis="wght">
|
||||||
|
<mapping from="-1.0" to="-1.0"/>
|
||||||
|
<mapping from="0.0" to="0.0"/>
|
||||||
|
<mapping from="0.3" to="0.5"/>
|
||||||
|
<mapping from="1.0" to="1.0"/>
|
||||||
|
</segment>
|
||||||
|
</avar>
|
||||||
|
|
||||||
|
<fvar>
|
||||||
|
<Axis>
|
||||||
|
<AxisTag>wght</AxisTag>
|
||||||
|
<Flags>0x0</Flags>
|
||||||
|
<MinValue>100.0</MinValue>
|
||||||
|
<DefaultValue>400.0</DefaultValue>
|
||||||
|
<MaxValue>900.0</MaxValue>
|
||||||
|
<AxisNameID>257</AxisNameID>
|
||||||
|
</Axis>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="258">
|
||||||
|
<coord axis="wght" value="100.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="259">
|
||||||
|
<coord axis="wght" value="300.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="260">
|
||||||
|
<coord axis="wght" value="400.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="261">
|
||||||
|
<coord axis="wght" value="700.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
<NamedInstance flags="0x0" subfamilyNameID="262">
|
||||||
|
<coord axis="wght" value="900.0"/>
|
||||||
|
</NamedInstance>
|
||||||
|
</fvar>
|
||||||
|
|
||||||
|
</ttFont>
|
@ -485,6 +485,24 @@ class SubsetTest(unittest.TestCase):
|
|||||||
subset.main([fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"])
|
subset.main([fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"])
|
||||||
self.assertLess(modified, TTFont(subsetpath)['head'].modified)
|
self.assertLess(modified, TTFont(subsetpath)['head'].modified)
|
||||||
|
|
||||||
|
def test_recalc_max_context(self):
|
||||||
|
ttxpath = self.getpath("Lobster.subset.ttx")
|
||||||
|
font = TTFont()
|
||||||
|
font.importXML(ttxpath)
|
||||||
|
max_context = font['OS/2'].usMaxContext
|
||||||
|
_, fontpath = self.compile_font(ttxpath, ".otf")
|
||||||
|
subsetpath = self.temp_path(".otf")
|
||||||
|
|
||||||
|
# by default, the subsetter does not recalculate the usMaxContext
|
||||||
|
subset.main([fontpath, "--drop-tables+=GSUB,GPOS",
|
||||||
|
"--output-file=%s" % subsetpath])
|
||||||
|
self.assertEqual(max_context, TTFont(subsetpath)['OS/2'].usMaxContext)
|
||||||
|
|
||||||
|
subset.main([fontpath, "--recalc-max-context",
|
||||||
|
"--drop-tables+=GSUB,GPOS",
|
||||||
|
"--output-file=%s" % subsetpath])
|
||||||
|
self.assertEqual(0, TTFont(subsetpath)['OS/2'].usMaxContext)
|
||||||
|
|
||||||
def test_retain_gids_ttf(self):
|
def test_retain_gids_ttf(self):
|
||||||
_, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
_, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
||||||
font = TTFont(fontpath)
|
font = TTFont(fontpath)
|
||||||
@ -586,6 +604,20 @@ class SubsetTest(unittest.TestCase):
|
|||||||
self.assertGreater(len(cs["A"].program), 0)
|
self.assertGreater(len(cs["A"].program), 0)
|
||||||
self.assertEqual(cs["glyph00002"].program, [])
|
self.assertEqual(cs["glyph00002"].program, [])
|
||||||
|
|
||||||
|
def test_HVAR_VVAR(self):
|
||||||
|
_, fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf")
|
||||||
|
subsetpath = self.temp_path(".ttf")
|
||||||
|
subset.main([fontpath, "--text=BD", "--output-file=%s" % subsetpath])
|
||||||
|
subsetfont = TTFont(subsetpath)
|
||||||
|
self.expect_ttx(subsetfont, self.getpath("expect_HVVAR.ttx"), ["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"])
|
||||||
|
|
||||||
|
def test_HVAR_VVAR_retain_gids(self):
|
||||||
|
_, fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf")
|
||||||
|
subsetpath = self.temp_path(".ttf")
|
||||||
|
subset.main([fontpath, "--text=BD", "--retain-gids", "--output-file=%s" % subsetpath])
|
||||||
|
subsetfont = TTFont(subsetpath)
|
||||||
|
self.expect_ttx(subsetfont, self.getpath("expect_HVVAR_retain_gids.ttx"), ["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(unittest.main())
|
sys.exit(unittest.main())
|
||||||
|
229
Tests/varLib/data/TestSparseCFF2VF.designspace
Normal file
229
Tests/varLib/data/TestSparseCFF2VF.designspace
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<designspace format="3">
|
||||||
|
<axes>
|
||||||
|
<axis default="200" maximum="900" minimum="200" name="weight" tag="wght">
|
||||||
|
<map input="200" output="0" /> <!-- ExtraLight -->
|
||||||
|
<map input="300" output="160" /> <!-- Light -->
|
||||||
|
<map input="350" output="320" /> <!-- Normal -->
|
||||||
|
<map input="400" output="390" /> <!-- Regular -->
|
||||||
|
<map input="500" output="560" /> <!-- Medium -->
|
||||||
|
<map input="700" output="780" /> <!-- Bold -->
|
||||||
|
<map input="900" output="1000" /><!-- Heavy -->
|
||||||
|
</axis>
|
||||||
|
</axes>
|
||||||
|
<sources>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w0.00.ufo" stylename="w0.00">
|
||||||
|
<info copy="1" />
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="0.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w439.00.ufo" stylename="w439.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="439.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w440.00.ufo" stylename="w440.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="440.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w599.00.ufo" stylename="w599.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="599.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w600.00.ufo" stylename="w600.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="600.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w669.00.ufo" stylename="w669.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="669.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w670.00.ufo" stylename="w670.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="670.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w799.00.ufo" stylename="w799.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="799.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w800.00.ufo" stylename="w800.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="800.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w889.00.ufo" stylename="w889.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="889.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w890.00.ufo" stylename="w890.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="890.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
<source filename="master_sparse_cff2/MasterSet_Kanji-w1000.00.ufo" stylename="w1000.00">
|
||||||
|
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="1000.00" />
|
||||||
|
</location>
|
||||||
|
</source>
|
||||||
|
</sources>
|
||||||
|
<instances>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w0.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w0.00" stylename="Kanji-w0.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="0" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w239.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w239.00" stylename="Kanji-w239.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="239" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w240.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w240.00" stylename="Kanji-w240.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="240" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w439.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w439.00" stylename="Kanji-w439.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="439" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w440.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w440.00" stylename="Kanji-w440.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="440" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
|
||||||
|
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w499.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w499.00" stylename="Kanji-w499.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="499" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w500.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w500.00" stylename="Kanji-w500.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="500" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
|
||||||
|
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w599.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w599.00" stylename="Kanji-w599.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="599" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w600.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w600.00" stylename="Kanji-w600.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="600" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
|
||||||
|
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w669.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w669.00" stylename="Kanji-w669.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="669" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w670.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w670.00" stylename="Kanji-w670.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="670" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
|
||||||
|
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w699.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w699.00" stylename="Kanji-w699.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="699" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w700.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w700.00" stylename="Kanji-w700.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="700" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
|
||||||
|
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-799.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-799.00" stylename="Kanji-799.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="799" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w800.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w800.00" stylename="Kanji-w800.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="800" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
|
||||||
|
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w889.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w889.00" stylename="Kanji-w889.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="889" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w890.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w890.00" stylename="Kanji-w890.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="890" />
|
||||||
|
</location>
|
||||||
|
<kerning />
|
||||||
|
<info />
|
||||||
|
</instance>
|
||||||
|
|
||||||
|
|
||||||
|
<instance familyname="SHSansJPVFTest" filename="instances/SHSansJPVFTest-Kanji-w1000.00.otf" postscriptfontname="SHSansJPVFTest-Kanji-w1000.00" stylename="Kanji-w1000.00">
|
||||||
|
<location>
|
||||||
|
<dimension name="weight" xvalue="1000" />
|
||||||
|
</location>
|
||||||
|
</instance>
|
||||||
|
</instances>
|
||||||
|
</designspace>
|
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w0.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w0.00.otf
Normal file
Binary file not shown.
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w439.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w439.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w440.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w440.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w599.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w599.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w600.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w600.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w669.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w669.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w670.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w670.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w799.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w799.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w800.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w800.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w889.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w889.00.otf
Normal file
Binary file not shown.
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w890.00.otf
Normal file
BIN
Tests/varLib/data/master_sparse_cff2/MasterSet_Kanji-w890.00.otf
Normal file
Binary file not shown.
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ttFont sfntVersion="OTTO" ttLibVersion="3.32">
|
<ttFont sfntVersion="OTTO" ttLibVersion="3.38">
|
||||||
|
|
||||||
<fvar>
|
<fvar>
|
||||||
|
|
||||||
@ -67,29 +67,29 @@
|
|||||||
<BlueValues>
|
<BlueValues>
|
||||||
<blend value="-12 0 0"/>
|
<blend value="-12 0 0"/>
|
||||||
<blend value="0 0 0"/>
|
<blend value="0 0 0"/>
|
||||||
<blend value="486 -8 14"/>
|
<blend value="486 -8 -8"/>
|
||||||
<blend value="498 0 0"/>
|
<blend value="498 0 0"/>
|
||||||
<blend value="574 4 -8"/>
|
<blend value="574 4 4"/>
|
||||||
<blend value="586 0 0"/>
|
<blend value="586 0 0"/>
|
||||||
<blend value="638 6 -10"/>
|
<blend value="638 6 6"/>
|
||||||
<blend value="650 0 0"/>
|
<blend value="650 0 0"/>
|
||||||
<blend value="656 2 -2"/>
|
<blend value="656 2 2"/>
|
||||||
<blend value="668 0 0"/>
|
<blend value="668 0 0"/>
|
||||||
<blend value="712 6 -10"/>
|
<blend value="712 6 6"/>
|
||||||
<blend value="724 0 0"/>
|
<blend value="724 0 0"/>
|
||||||
</BlueValues>
|
</BlueValues>
|
||||||
<OtherBlues>
|
<OtherBlues>
|
||||||
<blend value="-217 -17 29"/>
|
<blend value="-217 -17 -17"/>
|
||||||
<blend value="-205 0 0"/>
|
<blend value="-205 0 0"/>
|
||||||
</OtherBlues>
|
</OtherBlues>
|
||||||
<BlueScale value="0.0625"/>
|
<BlueScale value="0.0625"/>
|
||||||
<BlueShift value="7"/>
|
<BlueShift value="7"/>
|
||||||
<BlueFuzz value="0"/>
|
<BlueFuzz value="0"/>
|
||||||
<StdHW>
|
<StdHW>
|
||||||
<blend value="67 -39.0 67.0"/>
|
<blend value="67 -39.0 -39.0"/>
|
||||||
</StdHW>
|
</StdHW>
|
||||||
<StdVW>
|
<StdVW>
|
||||||
<blend value="85 -51.0 87.0"/>
|
<blend value="85 -51.0 -51.0"/>
|
||||||
</StdVW>
|
</StdVW>
|
||||||
</Private>
|
</Private>
|
||||||
</FontDict>
|
</FontDict>
|
||||||
|
1301
Tests/varLib/data/test_results/TestSparseCFF2VF.ttx
Normal file
1301
Tests/varLib/data/test_results/TestSparseCFF2VF.ttx
Normal file
File diff suppressed because it is too large
Load Diff
@ -244,6 +244,23 @@ class BuildTest(unittest.TestCase):
|
|||||||
self.expect_ttx(varfont, expected_ttx_path, tables)
|
self.expect_ttx(varfont, expected_ttx_path, tables)
|
||||||
self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
|
self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
|
||||||
|
|
||||||
|
def test_varlib_build_sparse_CFF2(self):
|
||||||
|
ds_path = self.get_test_input('TestSparseCFF2VF.designspace')
|
||||||
|
suffix = '.otf'
|
||||||
|
expected_ttx_name = 'TestSparseCFF2VF'
|
||||||
|
tables = ["fvar", "CFF2"]
|
||||||
|
|
||||||
|
finder = lambda s: s.replace('.ufo', suffix)
|
||||||
|
varfont, model, _ = build(ds_path, finder)
|
||||||
|
# some data (e.g. counts printed in TTX inline comments) is only
|
||||||
|
# calculated at compile time, so before we can compare the TTX
|
||||||
|
# dumps we need to save to a temporary stream, and realod the font
|
||||||
|
varfont = reload_font(varfont)
|
||||||
|
|
||||||
|
expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
|
||||||
|
self.expect_ttx(varfont, expected_ttx_path, tables)
|
||||||
|
self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
|
||||||
|
|
||||||
def test_varlib_main_ttf(self):
|
def test_varlib_main_ttf(self):
|
||||||
"""Mostly for testing varLib.main()
|
"""Mostly for testing varLib.main()
|
||||||
"""
|
"""
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 3.40.1.dev0
|
current_version = 3.41.1.dev0
|
||||||
commit = True
|
commit = True
|
||||||
tag = False
|
tag = False
|
||||||
tag_name = {new_version}
|
tag_name = {new_version}
|
||||||
|
2
setup.py
2
setup.py
@ -352,7 +352,7 @@ def find_data_files(manpath="share/man"):
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="fonttools",
|
name="fonttools",
|
||||||
version="3.40.1.dev0",
|
version="3.41.1.dev0",
|
||||||
description="Tools to manipulate font files",
|
description="Tools to manipulate font files",
|
||||||
author="Just van Rossum",
|
author="Just van Rossum",
|
||||||
author_email="just@letterror.com",
|
author_email="just@letterror.com",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user