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