• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from collections import namedtuple
2from fontTools.cffLib import (
3	maxStackLimit,
4	TopDictIndex,
5	buildOrder,
6	topDictOperators,
7	topDictOperators2,
8	privateDictOperators,
9	privateDictOperators2,
10	FDArrayIndex,
11	FontDict,
12	VarStoreData
13)
14from io import BytesIO
15from fontTools.cffLib.specializer import (
16	specializeCommands, commandsToProgram)
17from fontTools.ttLib import newTable
18from fontTools import varLib
19from fontTools.varLib.models import allEqual
20from fontTools.misc.roundTools import roundFunc
21from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor
22from fontTools.pens.t2CharStringPen import T2CharStringPen
23from functools import partial
24
25from .errors import (
26	VarLibCFFDictMergeError, VarLibCFFPointTypeMergeError,
27	VarLibCFFHintTypeMergeError,VarLibMergeError)
28
29
30# Backwards compatibility
31MergeDictError = VarLibCFFDictMergeError
32MergeTypeError = VarLibCFFPointTypeMergeError
33
34
35def addCFFVarStore(varFont, varModel, varDataList, masterSupports):
36	fvarTable = varFont['fvar']
37	axisKeys = [axis.axisTag for axis in fvarTable.axes]
38	varTupleList = varLib.builder.buildVarRegionList(masterSupports, axisKeys)
39	varStoreCFFV = varLib.builder.buildVarStore(varTupleList, varDataList)
40
41	topDict = varFont['CFF2'].cff.topDictIndex[0]
42	topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV)
43	if topDict.FDArray[0].vstore is None:
44		fdArray = topDict.FDArray
45		for fontDict in fdArray:
46			if hasattr(fontDict, "Private"):
47				fontDict.Private.vstore = topDict.VarStore
48
49
50def lib_convertCFFToCFF2(cff, otFont):
51	# This assumes a decompiled CFF table.
52	cff2GetGlyphOrder = cff.otFont.getGlyphOrder
53	topDictData = TopDictIndex(None, cff2GetGlyphOrder, None)
54	topDictData.items = cff.topDictIndex.items
55	cff.topDictIndex = topDictData
56	topDict = topDictData[0]
57	if hasattr(topDict, 'Private'):
58		privateDict = topDict.Private
59	else:
60		privateDict = None
61	opOrder = buildOrder(topDictOperators2)
62	topDict.order = opOrder
63	topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
64	if not hasattr(topDict, "FDArray"):
65		fdArray = topDict.FDArray = FDArrayIndex()
66		fdArray.strings = None
67		fdArray.GlobalSubrs = topDict.GlobalSubrs
68		topDict.GlobalSubrs.fdArray = fdArray
69		charStrings = topDict.CharStrings
70		if charStrings.charStringsAreIndexed:
71			charStrings.charStringsIndex.fdArray = fdArray
72		else:
73			charStrings.fdArray = fdArray
74		fontDict = FontDict()
75		fontDict.setCFF2(True)
76		fdArray.append(fontDict)
77		fontDict.Private = privateDict
78		privateOpOrder = buildOrder(privateDictOperators2)
79		if privateDict is not None:
80			for entry in privateDictOperators:
81				key = entry[1]
82				if key not in privateOpOrder:
83					if key in privateDict.rawDict:
84						# print "Removing private dict", key
85						del privateDict.rawDict[key]
86					if hasattr(privateDict, key):
87						delattr(privateDict, key)
88						# print "Removing privateDict attr", key
89	else:
90		# clean up the PrivateDicts in the fdArray
91		fdArray = topDict.FDArray
92		privateOpOrder = buildOrder(privateDictOperators2)
93		for fontDict in fdArray:
94			fontDict.setCFF2(True)
95			for key in list(fontDict.rawDict.keys()):
96				if key not in fontDict.order:
97					del fontDict.rawDict[key]
98					if hasattr(fontDict, key):
99						delattr(fontDict, key)
100
101			privateDict = fontDict.Private
102			for entry in privateDictOperators:
103				key = entry[1]
104				if key not in privateOpOrder:
105					if key in privateDict.rawDict:
106						# print "Removing private dict", key
107						del privateDict.rawDict[key]
108					if hasattr(privateDict, key):
109						delattr(privateDict, key)
110						# print "Removing privateDict attr", key
111	# Now delete up the decrecated topDict operators from CFF 1.0
112	for entry in topDictOperators:
113		key = entry[1]
114		if key not in opOrder:
115			if key in topDict.rawDict:
116				del topDict.rawDict[key]
117			if hasattr(topDict, key):
118				delattr(topDict, key)
119
120	# At this point, the Subrs and Charstrings are all still T2Charstring class
121	# easiest to fix this by compiling, then decompiling again
122	cff.major = 2
123	file = BytesIO()
124	cff.compile(file, otFont, isCFF2=True)
125	file.seek(0)
126	cff.decompile(file, otFont, isCFF2=True)
127
128
129def convertCFFtoCFF2(varFont):
130	# Convert base font to a single master CFF2 font.
131	cffTable = varFont['CFF ']
132	lib_convertCFFToCFF2(cffTable.cff, varFont)
133	newCFF2 = newTable("CFF2")
134	newCFF2.cff = cffTable.cff
135	varFont['CFF2'] = newCFF2
136	del varFont['CFF ']
137
138
139def conv_to_int(num):
140	if isinstance(num, float) and num.is_integer():
141		return int(num)
142	return num
143
144
145pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues",
146				   "FamilyOtherBlues", "BlueScale", "BlueShift",
147				   "BlueFuzz", "StdHW", "StdVW", "StemSnapH",
148				   "StemSnapV")
149
150
151def get_private(regionFDArrays, fd_index, ri, fd_map):
152	region_fdArray = regionFDArrays[ri]
153	region_fd_map = fd_map[fd_index]
154	if ri in region_fd_map:
155		region_fdIndex = region_fd_map[ri]
156		private = region_fdArray[region_fdIndex].Private
157	else:
158		private = None
159	return private
160
161
162def merge_PrivateDicts(top_dicts, vsindex_dict, var_model, fd_map):
163	"""
164	I step through the FontDicts in the FDArray of the varfont TopDict.
165	For each varfont FontDict:
166
167	* step through each key in FontDict.Private.
168	* For each key, step through each relevant source font Private dict, and
169		build a list of values to blend.
170
171	The 'relevant' source fonts are selected by first getting the right
172	submodel using ``vsindex_dict[vsindex]``. The indices of the
173	``subModel.locations`` are mapped to source font list indices by
174	assuming the latter order is the same as the order of the
175	``var_model.locations``. I can then get the index of each subModel
176	location in the list of ``var_model.locations``.
177	"""
178
179	topDict = top_dicts[0]
180	region_top_dicts = top_dicts[1:]
181	if hasattr(region_top_dicts[0], 'FDArray'):
182		regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts]
183	else:
184		regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts]
185	for fd_index, font_dict in enumerate(topDict.FDArray):
186		private_dict = font_dict.Private
187		vsindex = getattr(private_dict, 'vsindex', 0)
188		# At the moment, no PrivateDict has a vsindex key, but let's support
189		# how it should work. See comment at end of
190		# merge_charstrings() - still need to optimize use of vsindex.
191		sub_model, _ = vsindex_dict[vsindex]
192		master_indices = []
193		for loc in sub_model.locations[1:]:
194			i = var_model.locations.index(loc) - 1
195			master_indices.append(i)
196		pds = [private_dict]
197		last_pd = private_dict
198		for ri in master_indices:
199			pd = get_private(regionFDArrays, fd_index, ri, fd_map)
200			# If the region font doesn't have this FontDict, just reference
201			# the last one used.
202			if pd is None:
203				pd = last_pd
204			else:
205				last_pd = pd
206			pds.append(pd)
207		num_masters = len(pds)
208		for key, value in private_dict.rawDict.items():
209			dataList = []
210			if key not in pd_blend_fields:
211				continue
212			if isinstance(value, list):
213				try:
214					values = [pd.rawDict[key] for pd in pds]
215				except KeyError:
216					print(
217						"Warning: {key} in default font Private dict is "
218						"missing from another font, and was "
219						"discarded.".format(key=key))
220					continue
221				try:
222					values = zip(*values)
223				except IndexError:
224					raise VarLibCFFDictMergeError(key, value, values)
225				"""
226				Row 0 contains the first  value from each master.
227				Convert each row from absolute values to relative
228				values from the previous row.
229				e.g for three masters,	a list of values was:
230				master 0 OtherBlues = [-217,-205]
231				master 1 OtherBlues = [-234,-222]
232				master 1 OtherBlues = [-188,-176]
233				The call to zip() converts this to:
234				[(-217, -234, -188), (-205, -222, -176)]
235				and is converted finally to:
236				OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]]
237				"""
238				prev_val_list = [0] * num_masters
239				any_points_differ = False
240				for val_list in values:
241					rel_list = [(val - prev_val_list[i]) for (
242							i, val) in enumerate(val_list)]
243					if (not any_points_differ) and not allEqual(rel_list):
244						any_points_differ = True
245					prev_val_list = val_list
246					deltas = sub_model.getDeltas(rel_list)
247					# For PrivateDict BlueValues, the default font
248					# values are absolute, not relative to the prior value.
249					deltas[0] = val_list[0]
250					dataList.append(deltas)
251				# If there are no blend values,then
252				# we can collapse the blend lists.
253				if not any_points_differ:
254					dataList = [data[0] for data in dataList]
255			else:
256				values = [pd.rawDict[key] for pd in pds]
257				if not allEqual(values):
258					dataList = sub_model.getDeltas(values)
259				else:
260					dataList = values[0]
261
262			# Convert numbers with no decimal part to an int
263			if isinstance(dataList, list):
264				for i, item in enumerate(dataList):
265					if isinstance(item, list):
266						for j, jtem in enumerate(item):
267							dataList[i][j] = conv_to_int(jtem)
268					else:
269						dataList[i] = conv_to_int(item)
270			else:
271				dataList = conv_to_int(dataList)
272
273			private_dict.rawDict[key] = dataList
274
275
276def _cff_or_cff2(font):
277	if "CFF " in font:
278		return font["CFF "]
279	return font["CFF2"]
280
281
282def getfd_map(varFont, fonts_list):
283	""" Since a subset source font may have fewer FontDicts in their
284	FDArray than the default font, we have to match up the FontDicts in
285	the different fonts . We do this with the FDSelect array, and by
286	assuming that the same glyph will reference  matching FontDicts in
287	each source font. We return a mapping from fdIndex in the default
288	font to a dictionary which maps each master list index of each
289	region font to the equivalent fdIndex in the region font."""
290	fd_map = {}
291	default_font = fonts_list[0]
292	region_fonts = fonts_list[1:]
293	num_regions = len(region_fonts)
294	topDict = _cff_or_cff2(default_font).cff.topDictIndex[0]
295	if not hasattr(topDict, 'FDSelect'):
296		# All glyphs reference only one FontDict.
297		# Map the FD index for regions to index 0.
298		fd_map[0] = {ri:0 for ri in range(num_regions)}
299		return fd_map
300
301	gname_mapping = {}
302	default_fdSelect = topDict.FDSelect
303	glyphOrder = default_font.getGlyphOrder()
304	for gid, fdIndex in enumerate(default_fdSelect):
305		gname_mapping[glyphOrder[gid]] = fdIndex
306		if fdIndex not in fd_map:
307			fd_map[fdIndex] = {}
308	for ri, region_font in enumerate(region_fonts):
309		region_glyphOrder = region_font.getGlyphOrder()
310		region_topDict = _cff_or_cff2(region_font).cff.topDictIndex[0]
311		if not hasattr(region_topDict, 'FDSelect'):
312			# All the glyphs share the same FontDict. Pick any glyph.
313			default_fdIndex = gname_mapping[region_glyphOrder[0]]
314			fd_map[default_fdIndex][ri] = 0
315		else:
316			region_fdSelect = region_topDict.FDSelect
317			for gid, fdIndex in enumerate(region_fdSelect):
318				default_fdIndex = gname_mapping[region_glyphOrder[gid]]
319				region_map = fd_map[default_fdIndex]
320				if ri not in region_map:
321					region_map[ri] = fdIndex
322	return fd_map
323
324
325CVarData = namedtuple('CVarData', 'varDataList masterSupports vsindex_dict')
326def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder):
327	topDict = varFont['CFF2'].cff.topDictIndex[0]
328	top_dicts = [topDict] + [
329					_cff_or_cff2(ttFont).cff.topDictIndex[0]
330					for ttFont in ordered_fonts_list[1:]
331					]
332	num_masters = len(model.mapping)
333	cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model)
334	fd_map = getfd_map(varFont, ordered_fonts_list)
335	merge_PrivateDicts(top_dicts, cvData.vsindex_dict, model, fd_map)
336	addCFFVarStore(varFont, model, cvData.varDataList,
337		cvData.masterSupports)
338
339
340def _get_cs(charstrings, glyphName):
341	if glyphName not in charstrings:
342		return None
343	return charstrings[glyphName]
344
345def _add_new_vsindex(model, key, masterSupports, vsindex_dict,
346		vsindex_by_key, varDataList):
347	varTupleIndexes = []
348	for support in model.supports[1:]:
349		if support not in masterSupports:
350			masterSupports.append(support)
351		varTupleIndexes.append(masterSupports.index(support))
352	var_data = varLib.builder.buildVarData(varTupleIndexes, None, False)
353	vsindex = len(vsindex_dict)
354	vsindex_by_key[key] = vsindex
355	vsindex_dict[vsindex] = (model, [key])
356	varDataList.append(var_data)
357	return vsindex
358
359def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel):
360
361	vsindex_dict = {}
362	vsindex_by_key = {}
363	varDataList = []
364	masterSupports = []
365	default_charstrings = top_dicts[0].CharStrings
366	for gid, gname in enumerate(glyphOrder):
367		all_cs = [
368				_get_cs(td.CharStrings, gname)
369				for td in top_dicts]
370		if len([gs for gs in all_cs if gs is not None]) == 1:
371			continue
372		model, model_cs = masterModel.getSubModel(all_cs)
373		# create the first pass CFF2 charstring, from
374		# the default charstring.
375		default_charstring = model_cs[0]
376		var_pen = CFF2CharStringMergePen([], gname, num_masters, 0)
377		# We need to override outlineExtractor because these
378		# charstrings do have widths in the 'program'; we need to drop these
379		# values rather than post assertion error for them.
380		default_charstring.outlineExtractor = MergeOutlineExtractor
381		default_charstring.draw(var_pen)
382
383		# Add the coordinates from all the other regions to the
384		# blend lists in the CFF2 charstring.
385		region_cs = model_cs[1:]
386		for region_idx, region_charstring in enumerate(region_cs, start=1):
387			var_pen.restart(region_idx)
388			region_charstring.outlineExtractor = MergeOutlineExtractor
389			region_charstring.draw(var_pen)
390
391		# Collapse each coordinate list to a blend operator and its args.
392		new_cs = var_pen.getCharString(
393			private=default_charstring.private,
394			globalSubrs=default_charstring.globalSubrs,
395			var_model=model, optimize=True)
396		default_charstrings[gname] = new_cs
397
398		if (not var_pen.seen_moveto) or ('blend' not in new_cs.program):
399			# If this is not a marking glyph, or if there are no blend
400			# arguments, then we can use vsindex 0. No need to
401			# check if we need a new vsindex.
402			continue
403
404		# If the charstring required a new model, create
405		# a VarData table to go with, and set vsindex.
406		key = tuple(v is not None for v in all_cs)
407		try:
408			vsindex = vsindex_by_key[key]
409		except KeyError:
410			vsindex = _add_new_vsindex(model, key, masterSupports, vsindex_dict,
411				vsindex_by_key, varDataList)
412		# We do not need to check for an existing new_cs.private.vsindex,
413		# as we know it doesn't exist yet.
414		if vsindex != 0:
415			new_cs.program[:0] = [vsindex, 'vsindex']
416
417	# If there is no variation in any of the charstrings, then vsindex_dict
418	# never gets built. This could still be needed if there is variation
419	# in the PrivatDict, so we will build the default data for vsindex = 0.
420	if not vsindex_dict:
421		key = (True,) * num_masters
422		_add_new_vsindex(masterModel, key, masterSupports, vsindex_dict,
423			vsindex_by_key, varDataList)
424	cvData = CVarData(varDataList=varDataList, masterSupports=masterSupports,
425						vsindex_dict=vsindex_dict)
426	# XXX To do: optimize use of vsindex between the PrivateDicts and
427	# charstrings
428	return cvData
429
430
431class CFFToCFF2OutlineExtractor(T2OutlineExtractor):
432	""" This class is used to remove the initial width from the CFF
433	charstring without trying to add the width to self.nominalWidthX,
434	which is None. """
435	def popallWidth(self, evenOdd=0):
436		args = self.popall()
437		if not self.gotWidth:
438			if evenOdd ^ (len(args) % 2):
439				args = args[1:]
440			self.width = self.defaultWidthX
441			self.gotWidth = 1
442		return args
443
444
445class MergeOutlineExtractor(CFFToCFF2OutlineExtractor):
446	""" Used to extract the charstring commands - including hints - from a
447	CFF charstring in order to merge it as another set of region data
448	into a CFF2 variable font charstring."""
449
450	def __init__(self, pen, localSubrs, globalSubrs,
451			nominalWidthX, defaultWidthX, private=None):
452		super().__init__(pen, localSubrs,
453			globalSubrs, nominalWidthX, defaultWidthX, private)
454
455	def countHints(self):
456		args = self.popallWidth()
457		self.hintCount = self.hintCount + len(args) // 2
458		return args
459
460	def _hint_op(self, type, args):
461		self.pen.add_hint(type, args)
462
463	def op_hstem(self, index):
464		args = self.countHints()
465		self._hint_op('hstem', args)
466
467	def op_vstem(self, index):
468		args = self.countHints()
469		self._hint_op('vstem', args)
470
471	def op_hstemhm(self, index):
472		args = self.countHints()
473		self._hint_op('hstemhm', args)
474
475	def op_vstemhm(self, index):
476		args = self.countHints()
477		self._hint_op('vstemhm', args)
478
479	def _get_hintmask(self, index):
480		if not self.hintMaskBytes:
481			args = self.countHints()
482			if args:
483				self._hint_op('vstemhm', args)
484			self.hintMaskBytes = (self.hintCount + 7) // 8
485		hintMaskBytes, index = self.callingStack[-1].getBytes(index,
486			self.hintMaskBytes)
487		return index, hintMaskBytes
488
489	def op_hintmask(self, index):
490		index, hintMaskBytes = self._get_hintmask(index)
491		self.pen.add_hintmask('hintmask', [hintMaskBytes])
492		return hintMaskBytes, index
493
494	def op_cntrmask(self, index):
495		index, hintMaskBytes = self._get_hintmask(index)
496		self.pen.add_hintmask('cntrmask', [hintMaskBytes])
497		return hintMaskBytes, index
498
499
500class CFF2CharStringMergePen(T2CharStringPen):
501	"""Pen to merge Type 2 CharStrings.
502	"""
503	def __init__(
504				self, default_commands, glyphName, num_masters, master_idx,
505				roundTolerance=0.5):
506		super().__init__(
507							width=None,
508							glyphSet=None, CFF2=True,
509							roundTolerance=roundTolerance)
510		self.pt_index = 0
511		self._commands = default_commands
512		self.m_index = master_idx
513		self.num_masters = num_masters
514		self.prev_move_idx = 0
515		self.seen_moveto = False
516		self.glyphName = glyphName
517		self.round = roundFunc(roundTolerance, round=round)
518
519	def add_point(self, point_type, pt_coords):
520		if self.m_index == 0:
521			self._commands.append([point_type, [pt_coords]])
522		else:
523			cmd = self._commands[self.pt_index]
524			if cmd[0] != point_type:
525				raise VarLibCFFPointTypeMergeError(
526									point_type,
527									self.pt_index, len(cmd[1]),
528									cmd[0], self.glyphName)
529			cmd[1].append(pt_coords)
530		self.pt_index += 1
531
532	def add_hint(self, hint_type, args):
533		if self.m_index == 0:
534			self._commands.append([hint_type, [args]])
535		else:
536			cmd = self._commands[self.pt_index]
537			if cmd[0] != hint_type:
538				raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
539					cmd[0], self.glyphName)
540			cmd[1].append(args)
541		self.pt_index += 1
542
543	def add_hintmask(self, hint_type, abs_args):
544		# For hintmask, fonttools.cffLib.specializer.py expects
545		# each of these to be represented by two sequential commands:
546		# first holding only the operator name, with an empty arg list,
547		# second with an empty string as the op name, and the mask arg list.
548		if self.m_index == 0:
549			self._commands.append([hint_type, []])
550			self._commands.append(["", [abs_args]])
551		else:
552			cmd = self._commands[self.pt_index]
553			if cmd[0] != hint_type:
554				raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
555					cmd[0], self.glyphName)
556			self.pt_index += 1
557			cmd = self._commands[self.pt_index]
558			cmd[1].append(abs_args)
559		self.pt_index += 1
560
561	def _moveTo(self, pt):
562		if not self.seen_moveto:
563			self.seen_moveto = True
564		pt_coords = self._p(pt)
565		self.add_point('rmoveto', pt_coords)
566		# I set prev_move_idx here because add_point()
567		# can change self.pt_index.
568		self.prev_move_idx = self.pt_index - 1
569
570	def _lineTo(self, pt):
571		pt_coords = self._p(pt)
572		self.add_point('rlineto', pt_coords)
573
574	def _curveToOne(self, pt1, pt2, pt3):
575		_p = self._p
576		pt_coords = _p(pt1)+_p(pt2)+_p(pt3)
577		self.add_point('rrcurveto', pt_coords)
578
579	def _closePath(self):
580		pass
581
582	def _endPath(self):
583		pass
584
585	def restart(self, region_idx):
586		self.pt_index = 0
587		self.m_index = region_idx
588		self._p0 = (0, 0)
589
590	def getCommands(self):
591		return self._commands
592
593	def reorder_blend_args(self, commands, get_delta_func):
594		"""
595		We first re-order the master coordinate values.
596		For a moveto to lineto, the args are now arranged as::
597
598			[ [master_0 x,y], [master_1 x,y], [master_2 x,y] ]
599
600		We re-arrange this to::
601
602			[	[master_0 x, master_1 x, master_2 x],
603				[master_0 y, master_1 y, master_2 y]
604			]
605
606		If the master values are all the same, we collapse the list to
607		as single value instead of a list.
608
609		We then convert this to::
610
611			[ [master_0 x] + [x delta tuple] + [numBlends=1]
612			  [master_0 y] + [y delta tuple] + [numBlends=1]
613			]
614		"""
615		for cmd in commands:
616			# arg[i] is the set of arguments for this operator from master i.
617			args = cmd[1]
618			m_args = zip(*args)
619			# m_args[n] is now all num_master args for the i'th argument
620			# for this operation.
621			cmd[1] = list(m_args)
622		lastOp = None
623		for cmd in commands:
624			op = cmd[0]
625			# masks are represented by two cmd's: first has only op names,
626			# second has only args.
627			if lastOp in ['hintmask', 'cntrmask']:
628				coord = list(cmd[1])
629				if not allEqual(coord):
630					raise VarLibMergeError("Hintmask values cannot differ between source fonts.")
631				cmd[1] = [coord[0][0]]
632			else:
633				coords = cmd[1]
634				new_coords = []
635				for coord in coords:
636					if allEqual(coord):
637						new_coords.append(coord[0])
638					else:
639						# convert to deltas
640						deltas = get_delta_func(coord)[1:]
641						coord = [coord[0]] + deltas
642						new_coords.append(coord)
643				cmd[1] = new_coords
644			lastOp = op
645		return commands
646
647	def getCharString(
648					self, private=None, globalSubrs=None,
649					var_model=None, optimize=True):
650		commands = self._commands
651		commands = self.reorder_blend_args(commands, partial (var_model.getDeltas, round=self.round))
652		if optimize:
653			commands = specializeCommands(
654						commands, generalizeFirst=False,
655						maxstack=maxStackLimit)
656		program = commandsToProgram(commands)
657		charString = T2CharString(
658						program=program, private=private,
659						globalSubrs=globalSubrs)
660		return charString
661