fonttools/Lib/fontTools/cffLib/specializer.py

322 lines
10 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""T2CharString operator specializer and generalizer."""
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
def programToCommands(program):
"""Takes a T2CharString program list and returns list of commands.
Each command is a two-tuple of commandname,arg-list. The commandname might
be None if no commandname shall be emitted (used for glyph width (TODO),
hintmask/cntrmask argument, as well as stray arguments at the end of the
program (¯\_()_/¯)."""
commands = []
stack = []
it = iter(program)
for token in it:
if not isinstance(token, basestring):
stack.append(token)
continue
if token in {'hintmask', 'cntrmask'}:
if stack:
commands.append((None, stack))
commands.append((token, []))
commands.append((None, [next(it)]))
else:
commands.append((token,stack))
stack = []
if stack:
commands.append((None, stack))
return commands
def commandsToProgram(commands):
"""Takes a commands list as returned by programToCommands() and converts
it back to a T2CharString program list."""
program = []
for op,args in commands:
program.extend(args)
if op:
program.append(op)
return program
def _everyN(el, n):
"""Group the list el into groups of size n"""
if len(el) % n != 0: raise ValueError(args)
for i in range(0, len(el), n):
yield el[i:i+n]
2017-05-04 00:36:30 -07:00
class _GeneralizerDecombinerCommandsMap(object):
@staticmethod
def rmoveto(args):
if len(args) != 2: raise ValueError(args)
yield ('rmoveto', args)
@staticmethod
def hmoveto(args):
if len(args) != 1: raise ValueError(args)
yield ('rmoveto', [args[0], 0])
@staticmethod
def vmoveto(args):
if len(args) != 1: raise ValueError(args)
yield ('rmoveto', [0, args[0]])
@staticmethod
def rlineto(args):
for args in _everyN(args, 2):
yield ('rlineto', args)
@staticmethod
def hlineto(args):
it = iter(args)
while True:
yield ('rlineto', [next(it), 0])
yield ('rlineto', [0, next(it)])
@staticmethod
def vlineto(args):
it = iter(args)
while True:
yield ('rlineto', [0, next(it)])
yield ('rlineto', [next(it), 0])
@staticmethod
def rrcurveto(args):
for args in _everyN(args, 6):
yield ('rrcurveto', args)
@staticmethod
def hhcurveto(args):
if len(args) < 4 or len(args) % 4 > 1: raise ValueError(args)
if len(args) % 2 == 1:
yield ('rrcurveto', [args[1], args[0], args[2], args[3], args[4], 0])
args = args[5:]
for args in _everyN(args, 4):
yield ('rrcurveto', [args[0], 0, args[1], args[2], args[3], 0])
@staticmethod
def vvcurveto(args):
if len(args) < 4 or len(args) % 4 > 1: raise ValueError(args)
if len(args) % 2 == 1:
yield ('rrcurveto', [args[0], args[1], args[2], args[3], 0, args[4]])
args = args[5:]
for args in _everyN(args, 4):
yield ('rrcurveto', [0, args[0], args[1], args[2], 0, args[3]])
@staticmethod
def hvcurveto(args):
if len(args) < 4 or len(args) % 8 not in {0,1,4,5}: raise ValueError(args)
last_args = None
if len(args) % 2 == 1:
lastStraight = len(args) % 8 == 5
args, last_args = args[:-5], args[-5:]
it = _everyN(args, 4)
try:
while True:
args = next(it)
yield ('rrcurveto', [args[0], 0, args[1], args[2], 0, args[3]])
args = next(it)
yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], 0])
except StopIteration:
pass
if last_args:
args = last_args
if lastStraight:
yield ('rrcurveto', [args[0], 0, args[1], args[2], args[4], args[3]])
else:
yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], args[4]])
@staticmethod
def vhcurveto(args):
if len(args) < 4 or len(args) % 8 not in {0,1,4,5}: raise ValueError(args)
last_args = None
if len(args) % 2 == 1:
lastStraight = len(args) % 8 == 5
args, last_args = args[:-5], args[-5:]
it = _everyN(args, 4)
try:
while True:
args = next(it)
yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], 0])
args = next(it)
yield ('rrcurveto', [args[0], 0, args[1], args[2], 0, args[3]])
except StopIteration:
pass
if last_args:
args = last_args
if lastStraight:
yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], args[4]])
else:
yield ('rrcurveto', [args[0], 0, args[1], args[2], args[4], args[3]])
@staticmethod
def rcurveline(args):
if len(args) < 8 or len(args) % 6 != 2: raise ValueError(args)
args, last_args = args[:-2], args[-2:]
for args in _everyN(args, 6):
yield ('rrcurveto', args)
yield ('rlineto', last_args)
@staticmethod
def rlinecurve(args):
if len(args) < 8 or len(args) % 2 != 0: raise ValueError(args)
args, last_args = args[:-6], args[-6:]
for args in _everyN(args, 2):
yield ('rlineto', args)
yield ('rrcurveto', last_args)
2017-05-04 16:35:47 -07:00
def generalizeCommands(commands, ignoreErrors=True):
result = []
2017-05-04 00:36:30 -07:00
mapping = _GeneralizerDecombinerCommandsMap
for op,args in commands:
func = getattr(mapping, op if op else '', None)
if not func:
result.append((op,args))
continue
try:
for command in func(args):
result.append(command)
except ValueError:
if ignoreErrors:
2017-05-04 16:35:47 -07:00
# Store op as data, such that consumers of commands do not have to
# deal with incorrect number of arguments.
result.append((None,args))
result.append((None, [op]))
else:
raise
return result
def generalizeProgram(program, **kwargs):
return commandsToProgram(generalizeCommands(programToCommands(program), **kwargs))
def _categorizeVector(v):
"""
Takes X,Y vector v and returns one of r, h, v, or 0 depending on which
of X and/or Y are zero.
>>> _categorizeVector((0,0))
'0'
>>> _categorizeVector((1,0))
'h'
>>> _categorizeVector((0,2))
'v'
>>> _categorizeVector((1,2))
'r'
"""
return "rvh0"[(v[1]==0) * 2 + (v[0]==0)]
def specializeCommands(commands,
ignoreErrors=False,
generalizeFirst=True,
preserveTopology=False,
maxstack=48):
# We perform several rounds of optimizations. They are carefully ordered and are:
#
# 0. Generalize commands.
# This ensures that they are in our expected simple form, with each line/curve only
# having arguments for one segment, and using the generic form (rlineto/rrcurveto).
# If caller is sure the input is in this form, they can turn off generalization to
# save time.
#
# 1. Combine successive rmoveto operations.
#
# 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
# We specialize into some, made-up, varianats as well, which simplifies following
# passes.
#
# 3. Merge or delete redundant operations, if changing topology is allowed. OpenType
# spec declares point numbers in CFF undefined, so by default we happily change
# topology. If client relies on point numbers (in GPOS anchors, or for hinting
# purposes(what?)) they can turn this off.
#
# 4. Peephole optimization to revert back some of the h/v variants back into their
# original "relative" operator (rline/rrcurveto) if that saves a byte.
#
# 5. Resolve choices, ie. when same curve can be encoded in multiple ways using the
# same number of bytes, to maximize combining.
#
# 6. Combine adjacent operators when possible, minding not to go over max stack
# size.
#
# 7. Resolve any remaining made-up operators into real operators.
#
# 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. :-)
# 0. Generalize commands.
if generalizeFirst:
commands = generalizeCommands(commands, ignoreErrors=ignoreErrors)
else:
commands = commands[:] # Make copy since we modify in-place later.
# 1. Combine successive rmoveto operations.
for i in range(len(commands)-1, 0, -1):
if 'rmoveto' == commands[i][0] == commands[i-1][0]:
v1, v2 = commands[i-1][1], commands[i][1]
commands[i-1] = ('rmoveto', [v1[0]+v2[0], v1[1]+v2[1]])
del commands[i]
# 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
#
# We, in fact, specialize into more, made-up, variants that special-case when both
# X and Y components are zero. This simplifies the following optimization passes.
# This case is rare, but OCD does not let me skip it.
#
# After this round, we will have four variants that use the following mnemonics:
#
# - 'r' for relative, ie. non-zero X and non-zero Y,
# - 'h' for horizontal, ie. zero X and non-zero Y,
# - 'v' for vertical, ie. non-zero X and zero Y,
# - '0' for zeros, ie. zero X and zero Y.
#
# The zero pseudo-operators are not part of the spec, but help simplify the following
# optimization rounds. We resolve them at the end. So, after this, we will have four
# moveto and four lineto variants, and sixteen curveto variants. For example, a
# '0hcurveto' operator means a curve dx0,dy0,dx1,dy1,dx2,dy2,dx3,dy3 where dx0, dx1,
# and dy3 are zero but not dx3. An 'rvcurveto' means dx3 is zero but not dx0,dy0,dy3.
for i in range(len(commands)):
op,args = commands[i]
if op not in {'rmoveto', 'rlineto'}:
#c = _categorizeVector(args)
continue
if op != 'rrcurveto':
continue
# rrcurveto is the fun!
#new_commands, commands = commands[:1], commands[1:]
#for command in commands:
# new_commands.append(command)
#commands, new_commands = new_commands, None
2017-05-04 16:35:47 -07:00
return commands
def specializeProgram(program, **kwargs):
return commandsToProgram(specializeCommands(programToCommands(program), **kwargs))
if __name__ == '__main__':
import sys
if len(sys.argv) == 1:
import doctest
sys.exit(doctest.testmod().failed)
program = []
for token in sys.argv[1:]:
try:
token = int(token)
except ValueError:
try:
token = float(token)
except ValueError:
pass
program.append(token)
print("Program:"); print(program)
commands = programToCommands(program)
print("Commands:"); print(commands)
program2 = commandsToProgram(commands)
print("Program from commands:"); print(program2)
assert program == program2
print("Generalized program:"); print(generalizeProgram(program))
print("Specialized program:"); print(specializeProgram(program))