• 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		step through each key in FontDict.Private.
167		For each key, step through each relevant source font Private dict, and
168		build a list of values to blend.
169	The 'relevant' source fonts are selected by first getting the right
170	submodel using vsindex_dict[vsindex]. The indices of the
171	subModel.locations are mapped to source font list indices by
172	assuming the latter order is the same as the order of the
173	var_model.locations. I can then get the index of each subModel
174	location in the list of var_model.locations.
175	"""
176
177	topDict = top_dicts[0]
178	region_top_dicts = top_dicts[1:]
179	if hasattr(region_top_dicts[0], 'FDArray'):
180		regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts]
181	else:
182		regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts]
183	for fd_index, font_dict in enumerate(topDict.FDArray):
184		private_dict = font_dict.Private
185		vsindex = getattr(private_dict, 'vsindex', 0)
186		# At the moment, no PrivateDict has a vsindex key, but let's support
187		# how it should work. See comment at end of
188		# merge_charstrings() - still need to optimize use of vsindex.
189		sub_model, _ = vsindex_dict[vsindex]
190		master_indices = []
191		for loc in sub_model.locations[1:]:
192			i = var_model.locations.index(loc) - 1
193			master_indices.append(i)
194		pds = [private_dict]
195		last_pd = private_dict
196		for ri in master_indices:
197			pd = get_private(regionFDArrays, fd_index, ri, fd_map)
198			# If the region font doesn't have this FontDict, just reference
199			# the last one used.
200			if pd is None:
201				pd = last_pd
202			else:
203				last_pd = pd
204			pds.append(pd)
205		num_masters = len(pds)
206		for key, value in private_dict.rawDict.items():
207			dataList = []
208			if key not in pd_blend_fields:
209				continue
210			if isinstance(value, list):
211				try:
212					values = [pd.rawDict[key] for pd in pds]
213				except KeyError:
214					print(
215						"Warning: {key} in default font Private dict is "
216						"missing from another font, and was "
217						"discarded.".format(key=key))
218					continue
219				try:
220					values = zip(*values)
221				except IndexError:
222					raise VarLibCFFDictMergeError(key, value, values)
223				"""
224				Row 0 contains the first  value from each master.
225				Convert each row from absolute values to relative
226				values from the previous row.
227				e.g for three masters,	a list of values was:
228				master 0 OtherBlues = [-217,-205]
229				master 1 OtherBlues = [-234,-222]
230				master 1 OtherBlues = [-188,-176]
231				The call to zip() converts this to:
232				[(-217, -234, -188), (-205, -222, -176)]
233				and is converted finally to:
234				OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]]
235				"""
236				prev_val_list = [0] * num_masters
237				any_points_differ = False
238				for val_list in values:
239					rel_list = [(val - prev_val_list[i]) for (
240							i, val) in enumerate(val_list)]
241					if (not any_points_differ) and not allEqual(rel_list):
242						any_points_differ = True
243					prev_val_list = val_list
244					deltas = sub_model.getDeltas(rel_list)
245					# For PrivateDict BlueValues, the default font
246					# values are absolute, not relative to the prior value.
247					deltas[0] = val_list[0]
248					dataList.append(deltas)
249				# If there are no blend values,then
250				# we can collapse the blend lists.
251				if not any_points_differ:
252					dataList = [data[0] for data in dataList]
253			else:
254				values = [pd.rawDict[key] for pd in pds]
255				if not allEqual(values):
256					dataList = sub_model.getDeltas(values)
257				else:
258					dataList = values[0]
259
260			# Convert numbers with no decimal part to an int
261			if isinstance(dataList, list):
262				for i, item in enumerate(dataList):
263					if isinstance(item, list):
264						for j, jtem in enumerate(item):
265							dataList[i][j] = conv_to_int(jtem)
266					else:
267						dataList[i] = conv_to_int(item)
268			else:
269				dataList = conv_to_int(dataList)
270
271			private_dict.rawDict[key] = dataList
272
273
274def _cff_or_cff2(font):
275	if "CFF " in font:
276		return font["CFF "]
277	return font["CFF2"]
278
279
280def getfd_map(varFont, fonts_list):
281	""" Since a subset source font may have fewer FontDicts in their
282	FDArray than the default font, we have to match up the FontDicts in
283	the different fonts . We do this with the FDSelect array, and by
284	assuming that the same glyph will reference  matching FontDicts in
285	each source font. We return a mapping from fdIndex in the default
286	font to a dictionary which maps each master list index of each
287	region font to the equivalent fdIndex in the region font."""
288	fd_map = {}
289	default_font = fonts_list[0]
290	region_fonts = fonts_list[1:]
291	num_regions = len(region_fonts)
292	topDict = _cff_or_cff2(default_font).cff.topDictIndex[0]
293	if not hasattr(topDict, 'FDSelect'):
294		# All glyphs reference only one FontDict.
295		# Map the FD index for regions to index 0.
296		fd_map[0] = {ri:0 for ri in range(num_regions)}
297		return fd_map
298
299	gname_mapping = {}
300	default_fdSelect = topDict.FDSelect
301	glyphOrder = default_font.getGlyphOrder()
302	for gid, fdIndex in enumerate(default_fdSelect):
303		gname_mapping[glyphOrder[gid]] = fdIndex
304		if fdIndex not in fd_map:
305			fd_map[fdIndex] = {}
306	for ri, region_font in enumerate(region_fonts):
307		region_glyphOrder = region_font.getGlyphOrder()
308		region_topDict = _cff_or_cff2(region_font).cff.topDictIndex[0]
309		if not hasattr(region_topDict, 'FDSelect'):
310			# All the glyphs share the same FontDict. Pick any glyph.
311			default_fdIndex = gname_mapping[region_glyphOrder[0]]
312			fd_map[default_fdIndex][ri] = 0
313		else:
314			region_fdSelect = region_topDict.FDSelect
315			for gid, fdIndex in enumerate(region_fdSelect):
316				default_fdIndex = gname_mapping[region_glyphOrder[gid]]
317				region_map = fd_map[default_fdIndex]
318				if ri not in region_map:
319					region_map[ri] = fdIndex
320	return fd_map
321
322
323CVarData = namedtuple('CVarData', 'varDataList masterSupports vsindex_dict')
324def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder):
325	topDict = varFont['CFF2'].cff.topDictIndex[0]
326	top_dicts = [topDict] + [
327					_cff_or_cff2(ttFont).cff.topDictIndex[0]
328					for ttFont in ordered_fonts_list[1:]
329					]
330	num_masters = len(model.mapping)
331	cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model)
332	fd_map = getfd_map(varFont, ordered_fonts_list)
333	merge_PrivateDicts(top_dicts, cvData.vsindex_dict, model, fd_map)
334	addCFFVarStore(varFont, model, cvData.varDataList,
335		cvData.masterSupports)
336
337
338def _get_cs(charstrings, glyphName):
339	if glyphName not in charstrings:
340		return None
341	return charstrings[glyphName]
342
343def _add_new_vsindex(model, key, masterSupports, vsindex_dict,
344		vsindex_by_key, varDataList):
345	varTupleIndexes = []
346	for support in model.supports[1:]:
347		if support not in masterSupports:
348			masterSupports.append(support)
349		varTupleIndexes.append(masterSupports.index(support))
350	var_data = varLib.builder.buildVarData(varTupleIndexes, None, False)
351	vsindex = len(vsindex_dict)
352	vsindex_by_key[key] = vsindex
353	vsindex_dict[vsindex] = (model, [key])
354	varDataList.append(var_data)
355	return vsindex
356
357def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel):
358
359	vsindex_dict = {}
360	vsindex_by_key = {}
361	varDataList = []
362	masterSupports = []
363	default_charstrings = top_dicts[0].CharStrings
364	for gid, gname in enumerate(glyphOrder):
365		all_cs = [
366				_get_cs(td.CharStrings, gname)
367				for td in top_dicts]
368		if len([gs for gs in all_cs if gs is not None]) == 1:
369			continue
370		model, model_cs = masterModel.getSubModel(all_cs)
371		# create the first pass CFF2 charstring, from
372		# the default charstring.
373		default_charstring = model_cs[0]
374		var_pen = CFF2CharStringMergePen([], gname, num_masters, 0)
375		# We need to override outlineExtractor because these
376		# charstrings do have widths in the 'program'; we need to drop these
377		# values rather than post assertion error for them.
378		default_charstring.outlineExtractor = MergeOutlineExtractor
379		default_charstring.draw(var_pen)
380
381		# Add the coordinates from all the other regions to the
382		# blend lists in the CFF2 charstring.
383		region_cs = model_cs[1:]
384		for region_idx, region_charstring in enumerate(region_cs, start=1):
385			var_pen.restart(region_idx)
386			region_charstring.outlineExtractor = MergeOutlineExtractor
387			region_charstring.draw(var_pen)
388
389		# Collapse each coordinate list to a blend operator and its args.
390		new_cs = var_pen.getCharString(
391			private=default_charstring.private,
392			globalSubrs=default_charstring.globalSubrs,
393			var_model=model, optimize=True)
394		default_charstrings[gname] = new_cs
395
396		if (not var_pen.seen_moveto) or ('blend' not in new_cs.program):
397			# If this is not a marking glyph, or if there are no blend
398			# arguments, then we can use vsindex 0. No need to
399			# check if we need a new vsindex.
400			continue
401
402		# If the charstring required a new model, create
403		# a VarData table to go with, and set vsindex.
404		key = tuple(v is not None for v in all_cs)
405		try:
406			vsindex = vsindex_by_key[key]
407		except KeyError:
408			vsindex = _add_new_vsindex(model, key, masterSupports, vsindex_dict,
409				vsindex_by_key, varDataList)
410		# We do not need to check for an existing new_cs.private.vsindex,
411		# as we know it doesn't exist yet.
412		if vsindex != 0:
413			new_cs.program[:0] = [vsindex, 'vsindex']
414
415	# If there is no variation in any of the charstrings, then vsindex_dict
416	# never gets built. This could still be needed if there is variation
417	# in the PrivatDict, so we will build the default data for vsindex = 0.
418	if not vsindex_dict:
419		key = (True,) * num_masters
420		_add_new_vsindex(masterModel, key, masterSupports, vsindex_dict,
421			vsindex_by_key, varDataList)
422	cvData = CVarData(varDataList=varDataList, masterSupports=masterSupports,
423						vsindex_dict=vsindex_dict)
424	# XXX To do: optimize use of vsindex between the PrivateDicts and
425	# charstrings
426	return cvData
427
428
429class CFFToCFF2OutlineExtractor(T2OutlineExtractor):
430	""" This class is used to remove the initial width from the CFF
431	charstring without trying to add the width to self.nominalWidthX,
432	which is None. """
433	def popallWidth(self, evenOdd=0):
434		args = self.popall()
435		if not self.gotWidth:
436			if evenOdd ^ (len(args) % 2):
437				args = args[1:]
438			self.width = self.defaultWidthX
439			self.gotWidth = 1
440		return args
441
442
443class MergeOutlineExtractor(CFFToCFF2OutlineExtractor):
444	""" Used to extract the charstring commands - including hints - from a
445	CFF charstring in order to merge it as another set of region data
446	into a CFF2 variable font charstring."""
447
448	def __init__(self, pen, localSubrs, globalSubrs,
449			nominalWidthX, defaultWidthX, private=None):
450		super().__init__(pen, localSubrs,
451			globalSubrs, nominalWidthX, defaultWidthX, private)
452
453	def countHints(self):
454		args = self.popallWidth()
455		self.hintCount = self.hintCount + len(args) // 2
456		return args
457
458	def _hint_op(self, type, args):
459		self.pen.add_hint(type, args)
460
461	def op_hstem(self, index):
462		args = self.countHints()
463		self._hint_op('hstem', args)
464
465	def op_vstem(self, index):
466		args = self.countHints()
467		self._hint_op('vstem', args)
468
469	def op_hstemhm(self, index):
470		args = self.countHints()
471		self._hint_op('hstemhm', args)
472
473	def op_vstemhm(self, index):
474		args = self.countHints()
475		self._hint_op('vstemhm', args)
476
477	def _get_hintmask(self, index):
478		if not self.hintMaskBytes:
479			args = self.countHints()
480			if args:
481				self._hint_op('vstemhm', args)
482			self.hintMaskBytes = (self.hintCount + 7) // 8
483		hintMaskBytes, index = self.callingStack[-1].getBytes(index,
484			self.hintMaskBytes)
485		return index, hintMaskBytes
486
487	def op_hintmask(self, index):
488		index, hintMaskBytes = self._get_hintmask(index)
489		self.pen.add_hintmask('hintmask', [hintMaskBytes])
490		return hintMaskBytes, index
491
492	def op_cntrmask(self, index):
493		index, hintMaskBytes = self._get_hintmask(index)
494		self.pen.add_hintmask('cntrmask', [hintMaskBytes])
495		return hintMaskBytes, index
496
497
498class CFF2CharStringMergePen(T2CharStringPen):
499	"""Pen to merge Type 2 CharStrings.
500	"""
501	def __init__(
502				self, default_commands, glyphName, num_masters, master_idx,
503				roundTolerance=0.5):
504		super().__init__(
505							width=None,
506							glyphSet=None, CFF2=True,
507							roundTolerance=roundTolerance)
508		self.pt_index = 0
509		self._commands = default_commands
510		self.m_index = master_idx
511		self.num_masters = num_masters
512		self.prev_move_idx = 0
513		self.seen_moveto = False
514		self.glyphName = glyphName
515		self.round = roundFunc(roundTolerance, round=round)
516
517	def add_point(self, point_type, pt_coords):
518		if self.m_index == 0:
519			self._commands.append([point_type, [pt_coords]])
520		else:
521			cmd = self._commands[self.pt_index]
522			if cmd[0] != point_type:
523				raise VarLibCFFPointTypeMergeError(
524									point_type,
525									self.pt_index, len(cmd[1]),
526									cmd[0], self.glyphName)
527			cmd[1].append(pt_coords)
528		self.pt_index += 1
529
530	def add_hint(self, hint_type, args):
531		if self.m_index == 0:
532			self._commands.append([hint_type, [args]])
533		else:
534			cmd = self._commands[self.pt_index]
535			if cmd[0] != hint_type:
536				raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
537					cmd[0], self.glyphName)
538			cmd[1].append(args)
539		self.pt_index += 1
540
541	def add_hintmask(self, hint_type, abs_args):
542		# For hintmask, fonttools.cffLib.specializer.py expects
543		# each of these to be represented by two sequential commands:
544		# first holding only the operator name, with an empty arg list,
545		# second with an empty string as the op name, and the mask arg list.
546		if self.m_index == 0:
547			self._commands.append([hint_type, []])
548			self._commands.append(["", [abs_args]])
549		else:
550			cmd = self._commands[self.pt_index]
551			if cmd[0] != hint_type:
552				raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
553					cmd[0], self.glyphName)
554			self.pt_index += 1
555			cmd = self._commands[self.pt_index]
556			cmd[1].append(abs_args)
557		self.pt_index += 1
558
559	def _moveTo(self, pt):
560		if not self.seen_moveto:
561			self.seen_moveto = True
562		pt_coords = self._p(pt)
563		self.add_point('rmoveto', pt_coords)
564		# I set prev_move_idx here because add_point()
565		# can change self.pt_index.
566		self.prev_move_idx = self.pt_index - 1
567
568	def _lineTo(self, pt):
569		pt_coords = self._p(pt)
570		self.add_point('rlineto', pt_coords)
571
572	def _curveToOne(self, pt1, pt2, pt3):
573		_p = self._p
574		pt_coords = _p(pt1)+_p(pt2)+_p(pt3)
575		self.add_point('rrcurveto', pt_coords)
576
577	def _closePath(self):
578		pass
579
580	def _endPath(self):
581		pass
582
583	def restart(self, region_idx):
584		self.pt_index = 0
585		self.m_index = region_idx
586		self._p0 = (0, 0)
587
588	def getCommands(self):
589		return self._commands
590
591	def reorder_blend_args(self, commands, get_delta_func):
592		"""
593		We first re-order the master coordinate values.
594		For a moveto to lineto, the args are now arranged as:
595			[ [master_0 x,y], [master_1 x,y], [master_2 x,y] ]
596		We re-arrange this to
597		[	[master_0 x, master_1 x, master_2 x],
598			[master_0 y, master_1 y, master_2 y]
599		]
600		If the master values are all the same, we collapse the list to
601		as single value instead of a list.
602
603		We then convert this to:
604		[ [master_0 x] + [x delta tuple] + [numBlends=1]
605		  [master_0 y] + [y delta tuple] + [numBlends=1]
606		]
607		"""
608		for cmd in commands:
609			# arg[i] is the set of arguments for this operator from master i.
610			args = cmd[1]
611			m_args = zip(*args)
612			# m_args[n] is now all num_master args for the i'th argument
613			# for this operation.
614			cmd[1] = list(m_args)
615		lastOp = None
616		for cmd in commands:
617			op = cmd[0]
618			# masks are represented by two cmd's: first has only op names,
619			# second has only args.
620			if lastOp in ['hintmask', 'cntrmask']:
621				coord = list(cmd[1])
622				if not allEqual(coord):
623					raise VarLibMergeError("Hintmask values cannot differ between source fonts.")
624				cmd[1] = [coord[0][0]]
625			else:
626				coords = cmd[1]
627				new_coords = []
628				for coord in coords:
629					if allEqual(coord):
630						new_coords.append(coord[0])
631					else:
632						# convert to deltas
633						deltas = get_delta_func(coord)[1:]
634						coord = [coord[0]] + deltas
635						new_coords.append(coord)
636				cmd[1] = new_coords
637			lastOp = op
638		return commands
639
640	def getCharString(
641					self, private=None, globalSubrs=None,
642					var_model=None, optimize=True):
643		commands = self._commands
644		commands = self.reorder_blend_args(commands, partial (var_model.getDeltas, round=self.round))
645		if optimize:
646			commands = specializeCommands(
647						commands, generalizeFirst=False,
648						maxstack=maxStackLimit)
649		program = commandsToProgram(commands)
650		charString = T2CharString(
651						program=program, private=private,
652						globalSubrs=globalSubrs)
653		return charString
654