1# -*- coding: utf-8 -*-
2
3"""T2CharString operator specializer and generalizer."""
4
5from __future__ import print_function, division, absolute_import
6from fontTools.misc.py23 import *
7from fontTools.cffLib import maxStackLimit
8
9
10def stringToProgram(string):
11	if isinstance(string, basestring):
12		string = string.split()
13	program = []
14	for token in string:
15		try:
16			token = int(token)
17		except ValueError:
18			try:
19				token = float(token)
20			except ValueError:
21				pass
22		program.append(token)
23	return program
24
25
26def programToString(program):
27	return ' '.join(str(x) for x in program)
28
29
30def programToCommands(program, getNumRegions=None):
31	"""Takes a T2CharString program list and returns list of commands.
32	Each command is a two-tuple of commandname,arg-list.  The commandname might
33	be empty string if no commandname shall be emitted (used for glyph width,
34	hintmask/cntrmask argument, as well as stray arguments at the end of the
35	program (¯\_(ツ)_/¯).
36	'getNumRegions' may be None, or a callable object. It must return the
37	number of regions. 'getNumRegions' takes a single argument, vsindex. If
38	the vsindex argument is None, getNumRegions returns the default number
39	of regions for the charstring, else it returns the numRegions for
40	the vsindex.
41	The Charstring may or may not start with a width value. If the first
42	non-blend operator has an odd number of arguments, then the first argument is
43	a width, and is popped off. This is complicated with blend operators, as
44	there may be more than one before the first hint or moveto operator, and each
45	one reduces several arguments to just one list argument. We have to sum the
46	number of arguments that are not part of the blend arguments, and all the
47	'numBlends' values. We could instead have said that by definition, if there
48	is a blend operator, there is no width value, since CFF2 Charstrings don't
49	have width values. I discussed this with Behdad, and we are allowing for an
50	initial width value in this case because developers may assemble a CFF2
51	charstring from CFF Charstrings, which could have width values.
52	"""
53
54	seenWidthOp = False
55	vsIndex = None
56	lenBlendStack = 0
57	lastBlendIndex = 0
58	commands = []
59	stack = []
60	it = iter(program)
61
62	for token in it:
63		if not isinstance(token, basestring):
64			stack.append(token)
65			continue
66
67		if token == 'blend':
68			assert getNumRegions is not None
69			numSourceFonts = 1 + getNumRegions(vsIndex)
70			# replace the blend op args on the stack with a single list
71			# containing all the blend op args.
72			numBlends = stack[-1]
73			numBlendArgs = numBlends * numSourceFonts + 1
74			# replace first blend op by a list of the blend ops.
75			stack[-numBlendArgs:] = [stack[-numBlendArgs:]]
76			lenBlendStack += numBlends + len(stack) - 1
77			lastBlendIndex = len(stack)
78			# if a blend op exists, this is or will be a CFF2 charstring.
79			continue
80
81		elif token == 'vsindex':
82			vsIndex = stack[-1]
83			assert type(vsIndex) is int
84
85		elif (not seenWidthOp) and token in {'hstem', 'hstemhm', 'vstem', 'vstemhm',
86			'cntrmask', 'hintmask',
87			'hmoveto', 'vmoveto', 'rmoveto',
88			'endchar'}:
89			seenWidthOp = True
90			parity = token in {'hmoveto', 'vmoveto'}
91			if lenBlendStack:
92				# lenBlendStack has the number of args represented by the last blend
93				# arg and all the preceding args. We need to now add the number of
94				# args following the last blend arg.
95				numArgs = lenBlendStack + len(stack[lastBlendIndex:])
96			else:
97				numArgs = len(stack)
98			if numArgs and (numArgs % 2) ^ parity:
99				width = stack.pop(0)
100				commands.append(('', [width]))
101
102		if token in {'hintmask', 'cntrmask'}:
103			if stack:
104				commands.append(('', stack))
105			commands.append((token, []))
106			commands.append(('', [next(it)]))
107		else:
108			commands.append((token, stack))
109		stack = []
110	if stack:
111		commands.append(('', stack))
112	return commands
113
114
115def _flattenBlendArgs(args):
116	token_list = []
117	for arg in args:
118		if isinstance(arg, list):
119			token_list.extend(arg)
120			token_list.append('blend')
121		else:
122			token_list.append(arg)
123	return token_list
124
125def commandsToProgram(commands):
126	"""Takes a commands list as returned by programToCommands() and converts
127	it back to a T2CharString program list."""
128	program = []
129	for op,args in commands:
130		if any(isinstance(arg, list) for arg in args):
131			args = _flattenBlendArgs(args)
132		program.extend(args)
133		if op:
134			program.append(op)
135	return program
136
137
138def _everyN(el, n):
139	"""Group the list el into groups of size n"""
140	if len(el) % n != 0: raise ValueError(el)
141	for i in range(0, len(el), n):
142		yield el[i:i+n]
143
144
145class _GeneralizerDecombinerCommandsMap(object):
146
147	@staticmethod
148	def rmoveto(args):
149		if len(args) != 2: raise ValueError(args)
150		yield ('rmoveto', args)
151	@staticmethod
152	def hmoveto(args):
153		if len(args) != 1: raise ValueError(args)
154		yield ('rmoveto', [args[0], 0])
155	@staticmethod
156	def vmoveto(args):
157		if len(args) != 1: raise ValueError(args)
158		yield ('rmoveto', [0, args[0]])
159
160	@staticmethod
161	def rlineto(args):
162		if not args: raise ValueError(args)
163		for args in _everyN(args, 2):
164			yield ('rlineto', args)
165	@staticmethod
166	def hlineto(args):
167		if not args: raise ValueError(args)
168		it = iter(args)
169		try:
170			while True:
171				yield ('rlineto', [next(it), 0])
172				yield ('rlineto', [0, next(it)])
173		except StopIteration:
174			pass
175	@staticmethod
176	def vlineto(args):
177		if not args: raise ValueError(args)
178		it = iter(args)
179		try:
180			while True:
181				yield ('rlineto', [0, next(it)])
182				yield ('rlineto', [next(it), 0])
183		except StopIteration:
184			pass
185	@staticmethod
186	def rrcurveto(args):
187		if not args: raise ValueError(args)
188		for args in _everyN(args, 6):
189			yield ('rrcurveto', args)
190	@staticmethod
191	def hhcurveto(args):
192		if len(args) < 4 or len(args) % 4 > 1: raise ValueError(args)
193		if len(args) % 2 == 1:
194			yield ('rrcurveto', [args[1], args[0], args[2], args[3], args[4], 0])
195			args = args[5:]
196		for args in _everyN(args, 4):
197			yield ('rrcurveto', [args[0], 0, args[1], args[2], args[3], 0])
198	@staticmethod
199	def vvcurveto(args):
200		if len(args) < 4 or len(args) % 4 > 1: raise ValueError(args)
201		if len(args) % 2 == 1:
202			yield ('rrcurveto', [args[0], args[1], args[2], args[3], 0, args[4]])
203			args = args[5:]
204		for args in _everyN(args, 4):
205			yield ('rrcurveto', [0, args[0], args[1], args[2], 0, args[3]])
206	@staticmethod
207	def hvcurveto(args):
208		if len(args) < 4 or len(args) % 8 not in {0,1,4,5}: raise ValueError(args)
209		last_args = None
210		if len(args) % 2 == 1:
211			lastStraight = len(args) % 8 == 5
212			args, last_args = args[:-5], args[-5:]
213		it = _everyN(args, 4)
214		try:
215			while True:
216				args = next(it)
217				yield ('rrcurveto', [args[0], 0, args[1], args[2], 0, args[3]])
218				args = next(it)
219				yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], 0])
220		except StopIteration:
221			pass
222		if last_args:
223			args = last_args
224			if lastStraight:
225				yield ('rrcurveto', [args[0], 0, args[1], args[2], args[4], args[3]])
226			else:
227				yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], args[4]])
228	@staticmethod
229	def vhcurveto(args):
230		if len(args) < 4 or len(args) % 8 not in {0,1,4,5}: raise ValueError(args)
231		last_args = None
232		if len(args) % 2 == 1:
233			lastStraight = len(args) % 8 == 5
234			args, last_args = args[:-5], args[-5:]
235		it = _everyN(args, 4)
236		try:
237			while True:
238				args = next(it)
239				yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], 0])
240				args = next(it)
241				yield ('rrcurveto', [args[0], 0, args[1], args[2], 0, args[3]])
242		except StopIteration:
243			pass
244		if last_args:
245			args = last_args
246			if lastStraight:
247				yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], args[4]])
248			else:
249				yield ('rrcurveto', [args[0], 0, args[1], args[2], args[4], args[3]])
250
251	@staticmethod
252	def rcurveline(args):
253		if len(args) < 8 or len(args) % 6 != 2: raise ValueError(args)
254		args, last_args = args[:-2], args[-2:]
255		for args in _everyN(args, 6):
256			yield ('rrcurveto', args)
257		yield ('rlineto', last_args)
258	@staticmethod
259	def rlinecurve(args):
260		if len(args) < 8 or len(args) % 2 != 0: raise ValueError(args)
261		args, last_args = args[:-6], args[-6:]
262		for args in _everyN(args, 2):
263			yield ('rlineto', args)
264		yield ('rrcurveto', last_args)
265
266def _convertBlendOpToArgs(blendList):
267	# args is list of blend op args. Since we are supporting
268	# recursive blend op calls, some of these args may also
269	# be a list of blend op args, and need to be converted before
270	# we convert the current list.
271	if any([isinstance(arg, list) for arg in blendList]):
272		args =  [i for e in blendList for i in
273					(_convertBlendOpToArgs(e) if isinstance(e,list) else [e]) ]
274	else:
275		args = blendList
276
277	# We now know that blendList contains a blend op argument list, even if
278	# some of the args are lists that each contain a blend op argument list.
279	# 	Convert from:
280	# 		[default font arg sequence x0,...,xn] + [delta tuple for x0] + ... + [delta tuple for xn]
281	# 	to:
282	# 		[ [x0] + [delta tuple for x0],
283	#                 ...,
284	#          [xn] + [delta tuple for xn] ]
285	numBlends = args[-1]
286	# Can't use args.pop() when the args are being used in a nested list
287	# comprehension. See calling context
288	args = args[:-1]
289
290	numRegions = len(args)//numBlends - 1
291	if not (numBlends*(numRegions + 1) == len(args)):
292		raise ValueError(blendList)
293
294	defaultArgs = [[arg] for arg in args[:numBlends]]
295	deltaArgs = args[numBlends:]
296	numDeltaValues = len(deltaArgs)
297	deltaList = [ deltaArgs[i:i + numRegions] for i in range(0, numDeltaValues, numRegions) ]
298	blend_args = [ a + b for a, b in zip(defaultArgs,deltaList)]
299	return blend_args
300
301def generalizeCommands(commands, ignoreErrors=False):
302	result = []
303	mapping = _GeneralizerDecombinerCommandsMap
304	for op, args in commands:
305		# First, generalize any blend args in the arg list.
306		if any([isinstance(arg, list) for arg in args]):
307			try:
308				args = [n for arg in args for n in (_convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg])]
309			except ValueError:
310				if ignoreErrors:
311					# Store op as data, such that consumers of commands do not have to
312					# deal with incorrect number of arguments.
313					result.append(('', args))
314					result.append(('', [op]))
315				else:
316					raise
317
318		func = getattr(mapping, op, None)
319		if not func:
320			result.append((op,args))
321			continue
322		try:
323			for command in func(args):
324				result.append(command)
325		except ValueError:
326			if ignoreErrors:
327				# Store op as data, such that consumers of commands do not have to
328				# deal with incorrect number of arguments.
329				result.append(('', args))
330				result.append(('', [op]))
331			else:
332				raise
333	return result
334
335def generalizeProgram(program, getNumRegions=None, **kwargs):
336	return commandsToProgram(generalizeCommands(programToCommands(program, getNumRegions), **kwargs))
337
338
339def _categorizeVector(v):
340	"""
341	Takes X,Y vector v and returns one of r, h, v, or 0 depending on which
342	of X and/or Y are zero, plus tuple of nonzero ones.  If both are zero,
343	it returns a single zero still.
344
345	>>> _categorizeVector((0,0))
346	('0', (0,))
347	>>> _categorizeVector((1,0))
348	('h', (1,))
349	>>> _categorizeVector((0,2))
350	('v', (2,))
351	>>> _categorizeVector((1,2))
352	('r', (1, 2))
353	"""
354	if not v[0]:
355		if not v[1]:
356			return '0', v[:1]
357		else:
358			return 'v', v[1:]
359	else:
360		if not v[1]:
361			return 'h', v[:1]
362		else:
363			return 'r', v
364
365def _mergeCategories(a, b):
366	if a == '0': return b
367	if b == '0': return a
368	if a == b: return a
369	return None
370
371def _negateCategory(a):
372	if a == 'h': return 'v'
373	if a == 'v': return 'h'
374	assert a in '0r'
375	return a
376
377def _convertToBlendCmds(args):
378	# return a list of blend commands, and
379	# the remaining non-blended args, if any.
380	num_args = len(args)
381	stack_use = 0
382	new_args = []
383	i = 0
384	while i < num_args:
385		arg = args[i]
386		if not isinstance(arg, list):
387			new_args.append(arg)
388			i += 1
389			stack_use += 1
390		else:
391			prev_stack_use = stack_use
392			# The arg is a tuple of blend values.
393			# These are each (master 0,delta 1..delta n)
394			# Combine as many successive tuples as we can,
395			# up to the max stack limit.
396			num_sources = len(arg)
397			blendlist = [arg]
398			i += 1
399			stack_use += 1 + num_sources  # 1 for the num_blends arg
400			while (i < num_args) and isinstance(args[i], list):
401				blendlist.append(args[i])
402				i += 1
403				stack_use += num_sources
404				if stack_use + num_sources > maxStackLimit:
405					# if we are here, max stack is the CFF2 max stack.
406					# I use the CFF2 max stack limit here rather than
407					# the 'maxstack' chosen by the client, as the default
408					#  maxstack may have been used unintentionally. For all
409					# the other operators, this just produces a little less
410					# optimization, but here it puts a hard (and low) limit
411					# on the number of source fonts that can be used.
412					break
413			# blendList now contains as many single blend tuples as can be
414			# combined without exceeding the CFF2 stack limit.
415			num_blends = len(blendlist)
416			# append the 'num_blends' default font values
417			blend_args = []
418			for arg in blendlist:
419				blend_args.append(arg[0])
420			for arg in blendlist:
421				blend_args.extend(arg[1:])
422			blend_args.append(num_blends)
423			new_args.append(blend_args)
424			stack_use = prev_stack_use + num_blends
425
426	return new_args
427
428def _addArgs(a, b):
429	if isinstance(b, list):
430		if isinstance(a, list):
431			if len(a) != len(b):
432				raise ValueError()
433			return [_addArgs(va, vb) for va,vb in zip(a, b)]
434		else:
435			a, b = b, a
436	if isinstance(a, list):
437		return [_addArgs(a[0], b)] + a[1:]
438	return a + b
439
440
441def specializeCommands(commands,
442		       ignoreErrors=False,
443		       generalizeFirst=True,
444		       preserveTopology=False,
445		       maxstack=48):
446
447	# We perform several rounds of optimizations.  They are carefully ordered and are:
448	#
449	# 0. Generalize commands.
450	#    This ensures that they are in our expected simple form, with each line/curve only
451	#    having arguments for one segment, and using the generic form (rlineto/rrcurveto).
452	#    If caller is sure the input is in this form, they can turn off generalization to
453	#    save time.
454	#
455	# 1. Combine successive rmoveto operations.
456	#
457	# 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
458	#    We specialize into some, made-up, variants as well, which simplifies following
459	#    passes.
460	#
461	# 3. Merge or delete redundant operations, to the extent requested.
462	#    OpenType spec declares point numbers in CFF undefined.  As such, we happily
463	#    change topology.  If client relies on point numbers (in GPOS anchors, or for
464	#    hinting purposes(what?)) they can turn this off.
465	#
466	# 4. Peephole optimization to revert back some of the h/v variants back into their
467	#    original "relative" operator (rline/rrcurveto) if that saves a byte.
468	#
469	# 5. Combine adjacent operators when possible, minding not to go over max stack size.
470	#
471	# 6. Resolve any remaining made-up operators into real operators.
472	#
473	# I have convinced myself that this produces optimal bytecode (except for, possibly
474	# one byte each time maxstack size prohibits combining.)  YMMV, but you'd be wrong. :-)
475	# A dynamic-programming approach can do the same but would be significantly slower.
476	#
477	# 7. For any args which are blend lists, convert them to a blend command.
478
479
480	# 0. Generalize commands.
481	if generalizeFirst:
482		commands = generalizeCommands(commands, ignoreErrors=ignoreErrors)
483	else:
484		commands = list(commands) # Make copy since we modify in-place later.
485
486	# 1. Combine successive rmoveto operations.
487	for i in range(len(commands)-1, 0, -1):
488		if 'rmoveto' == commands[i][0] == commands[i-1][0]:
489			v1, v2 = commands[i-1][1], commands[i][1]
490			commands[i-1] = ('rmoveto', [v1[0]+v2[0], v1[1]+v2[1]])
491			del commands[i]
492
493	# 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
494	#
495	# We, in fact, specialize into more, made-up, variants that special-case when both
496	# X and Y components are zero.  This simplifies the following optimization passes.
497	# This case is rare, but OCD does not let me skip it.
498	#
499	# After this round, we will have four variants that use the following mnemonics:
500	#
501	#  - 'r' for relative,   ie. non-zero X and non-zero Y,
502	#  - 'h' for horizontal, ie. zero X and non-zero Y,
503	#  - 'v' for vertical,   ie. non-zero X and zero Y,
504	#  - '0' for zeros,      ie. zero X and zero Y.
505	#
506	# The '0' pseudo-operators are not part of the spec, but help simplify the following
507	# optimization rounds.  We resolve them at the end.  So, after this, we will have four
508	# moveto and four lineto variants:
509	#
510	#  - 0moveto, 0lineto
511	#  - hmoveto, hlineto
512	#  - vmoveto, vlineto
513	#  - rmoveto, rlineto
514	#
515	# and sixteen curveto variants.  For example, a '0hcurveto' operator means a curve
516	# dx0,dy0,dx1,dy1,dx2,dy2,dx3,dy3 where dx0, dx1, and dy3 are zero but not dx3.
517	# An 'rvcurveto' means dx3 is zero but not dx0,dy0,dy3.
518	#
519	# There are nine different variants of curves without the '0'.  Those nine map exactly
520	# to the existing curve variants in the spec: rrcurveto, and the four variants hhcurveto,
521	# vvcurveto, hvcurveto, and vhcurveto each cover two cases, one with an odd number of
522	# arguments and one without.  Eg. an hhcurveto with an extra argument (odd number of
523	# arguments) is in fact an rhcurveto.  The operators in the spec are designed such that
524	# all four of rhcurveto, rvcurveto, hrcurveto, and vrcurveto are encodable for one curve.
525	#
526	# Of the curve types with '0', the 00curveto is equivalent to a lineto variant.  The rest
527	# of the curve types with a 0 need to be encoded as a h or v variant.  Ie. a '0' can be
528	# thought of a "don't care" and can be used as either an 'h' or a 'v'.  As such, we always
529	# encode a number 0 as argument when we use a '0' variant.  Later on, we can just substitute
530	# the '0' with either 'h' or 'v' and it works.
531	#
532	# When we get to curve splines however, things become more complicated...  XXX finish this.
533	# There's one more complexity with splines.  If one side of the spline is not horizontal or
534	# vertical (or zero), ie. if it's 'r', then it limits which spline types we can encode.
535	# Only hhcurveto and vvcurveto operators can encode a spline starting with 'r', and
536	# only hvcurveto and vhcurveto operators can encode a spline ending with 'r'.
537	# This limits our merge opportunities later.
538	#
539	for i in range(len(commands)):
540		op,args = commands[i]
541
542		if op in {'rmoveto', 'rlineto'}:
543			c, args = _categorizeVector(args)
544			commands[i] = c+op[1:], args
545			continue
546
547		if op == 'rrcurveto':
548			c1, args1 = _categorizeVector(args[:2])
549			c2, args2 = _categorizeVector(args[-2:])
550			commands[i] = c1+c2+'curveto', args1+args[2:4]+args2
551			continue
552
553	# 3. Merge or delete redundant operations, to the extent requested.
554	#
555	# TODO
556	# A 0moveto that comes before all other path operations can be removed.
557	# though I find conflicting evidence for this.
558	#
559	# TODO
560	# "If hstem and vstem hints are both declared at the beginning of a
561	# CharString, and this sequence is followed directly by the hintmask or
562	# cntrmask operators, then the vstem hint operator (or, if applicable,
563	# the vstemhm operator) need not be included."
564	#
565	# "The sequence and form of a CFF2 CharString program may be represented as:
566	# {hs* vs* cm* hm* mt subpath}? {mt subpath}*"
567	#
568	# https://www.microsoft.com/typography/otspec/cff2charstr.htm#section3.1
569	#
570	# For Type2 CharStrings the sequence is:
571	# w? {hs* vs* cm* hm* mt subpath}? {mt subpath}* endchar"
572
573
574	# Some other redundancies change topology (point numbers).
575	if not preserveTopology:
576		for i in range(len(commands)-1, -1, -1):
577			op, args = commands[i]
578
579			# A 00curveto is demoted to a (specialized) lineto.
580			if op == '00curveto':
581				assert len(args) == 4
582				c, args = _categorizeVector(args[1:3])
583				op = c+'lineto'
584				commands[i] = op, args
585				# and then...
586
587			# A 0lineto can be deleted.
588			if op == '0lineto':
589				del commands[i]
590				continue
591
592			# Merge adjacent hlineto's and vlineto's.
593			# In CFF2 charstrings from variable fonts, each
594			# arg item may be a list of blendable values, one from
595			# each source font.
596			if (i and op in {'hlineto', 'vlineto'} and
597							(op == commands[i-1][0])):
598				_, other_args = commands[i-1]
599				assert len(args) == 1 and len(other_args) == 1
600				try:
601					new_args = [_addArgs(args[0], other_args[0])]
602				except ValueError:
603					continue
604				commands[i-1] = (op, new_args)
605				del commands[i]
606				continue
607
608	# 4. Peephole optimization to revert back some of the h/v variants back into their
609	#    original "relative" operator (rline/rrcurveto) if that saves a byte.
610	for i in range(1, len(commands)-1):
611		op,args = commands[i]
612		prv,nxt = commands[i-1][0], commands[i+1][0]
613
614		if op in {'0lineto', 'hlineto', 'vlineto'} and prv == nxt == 'rlineto':
615			assert len(args) == 1
616			args = [0, args[0]] if op[0] == 'v' else [args[0], 0]
617			commands[i] = ('rlineto', args)
618			continue
619
620		if op[2:] == 'curveto' and len(args) == 5 and prv == nxt == 'rrcurveto':
621			assert (op[0] == 'r') ^ (op[1] == 'r')
622			if op[0] == 'v':
623				pos = 0
624			elif op[0] != 'r':
625				pos = 1
626			elif op[1] == 'v':
627				pos = 4
628			else:
629				pos = 5
630			# Insert, while maintaining the type of args (can be tuple or list).
631			args = args[:pos] + type(args)((0,)) + args[pos:]
632			commands[i] = ('rrcurveto', args)
633			continue
634
635	# 5. Combine adjacent operators when possible, minding not to go over max stack size.
636	for i in range(len(commands)-1, 0, -1):
637		op1,args1 = commands[i-1]
638		op2,args2 = commands[i]
639		new_op = None
640
641		# Merge logic...
642		if {op1, op2} <= {'rlineto', 'rrcurveto'}:
643			if op1 == op2:
644				new_op = op1
645			else:
646				if op2 == 'rrcurveto' and len(args2) == 6:
647					new_op = 'rlinecurve'
648				elif len(args2) == 2:
649					new_op = 'rcurveline'
650
651		elif (op1, op2) in {('rlineto', 'rlinecurve'), ('rrcurveto', 'rcurveline')}:
652			new_op = op2
653
654		elif {op1, op2} == {'vlineto', 'hlineto'}:
655			new_op = op1
656
657		elif 'curveto' == op1[2:] == op2[2:]:
658			d0, d1 = op1[:2]
659			d2, d3 = op2[:2]
660
661			if d1 == 'r' or d2 == 'r' or d0 == d3 == 'r':
662				continue
663
664			d = _mergeCategories(d1, d2)
665			if d is None: continue
666			if d0 == 'r':
667				d = _mergeCategories(d, d3)
668				if d is None: continue
669				new_op = 'r'+d+'curveto'
670			elif d3 == 'r':
671				d0 = _mergeCategories(d0, _negateCategory(d))
672				if d0 is None: continue
673				new_op = d0+'r'+'curveto'
674			else:
675				d0 = _mergeCategories(d0, d3)
676				if d0 is None: continue
677				new_op = d0+d+'curveto'
678
679		# Make sure the stack depth does not exceed (maxstack - 1), so
680		# that subroutinizer can insert subroutine calls at any point.
681		if new_op and len(args1) + len(args2) < maxstack:
682			commands[i-1] = (new_op, args1+args2)
683			del commands[i]
684
685	# 6. Resolve any remaining made-up operators into real operators.
686	for i in range(len(commands)):
687		op,args = commands[i]
688
689		if op in {'0moveto', '0lineto'}:
690			commands[i] = 'h'+op[1:], args
691			continue
692
693		if op[2:] == 'curveto' and op[:2] not in {'rr', 'hh', 'vv', 'vh', 'hv'}:
694			op0, op1 = op[:2]
695			if (op0 == 'r') ^ (op1 == 'r'):
696				assert len(args) % 2 == 1
697			if op0 == '0': op0 = 'h'
698			if op1 == '0': op1 = 'h'
699			if op0 == 'r': op0 = op1
700			if op1 == 'r': op1 = _negateCategory(op0)
701			assert {op0,op1} <= {'h','v'}, (op0, op1)
702
703			if len(args) % 2:
704				if op0 != op1: # vhcurveto / hvcurveto
705					if (op0 == 'h') ^ (len(args) % 8 == 1):
706						# Swap last two args order
707						args = args[:-2]+args[-1:]+args[-2:-1]
708				else: # hhcurveto / vvcurveto
709					if op0 == 'h': # hhcurveto
710						# Swap first two args order
711						args = args[1:2]+args[:1]+args[2:]
712
713			commands[i] = op0+op1+'curveto', args
714			continue
715
716	# 7. For any series of args which are blend lists, convert the series to a single blend arg.
717	for i in range(len(commands)):
718		op, args = commands[i]
719		if any(isinstance(arg, list) for arg in args):
720			commands[i] = op, _convertToBlendCmds(args)
721
722	return commands
723
724def specializeProgram(program, getNumRegions=None, **kwargs):
725	return commandsToProgram(specializeCommands(programToCommands(program, getNumRegions), **kwargs))
726
727
728if __name__ == '__main__':
729	import sys
730	if len(sys.argv) == 1:
731		import doctest
732		sys.exit(doctest.testmod().failed)
733	program = stringToProgram(sys.argv[1:])
734	print("Program:"); print(programToString(program))
735	commands = programToCommands(program)
736	print("Commands:"); print(commands)
737	program2 = commandsToProgram(commands)
738	print("Program from commands:"); print(programToString(program2))
739	assert program == program2
740	print("Generalized program:"); print(programToString(generalizeProgram(program)))
741	print("Specialized program:"); print(programToString(specializeProgram(program)))
742