1import os
2from fontTools.misc.py23 import BytesIO
3from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor
4from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round
5from fontTools.cffLib import (
6	maxStackLimit,
7	TopDictIndex,
8	buildOrder,
9	topDictOperators,
10	topDictOperators2,
11	privateDictOperators,
12	privateDictOperators2,
13	FDArrayIndex,
14	FontDict,
15	VarStoreData
16)
17from fontTools.cffLib.specializer import (commandsToProgram, specializeCommands)
18from fontTools.ttLib import newTable
19from fontTools import varLib
20from fontTools.varLib.models import allEqual
21
22
23def addCFFVarStore(varFont, varModel):
24	supports = varModel.supports[1:]
25	fvarTable = varFont['fvar']
26	axisKeys = [axis.axisTag for axis in fvarTable.axes]
27	varTupleList = varLib.builder.buildVarRegionList(supports, axisKeys)
28	varTupleIndexes = list(range(len(supports)))
29	varDeltasCFFV = varLib.builder.buildVarData(varTupleIndexes, None, False)
30	varStoreCFFV = varLib.builder.buildVarStore(varTupleList, [varDeltasCFFV])
31
32	topDict = varFont['CFF2'].cff.topDictIndex[0]
33	topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV)
34
35
36def lib_convertCFFToCFF2(cff, otFont):
37	# This assumes a decompiled CFF table.
38	cff2GetGlyphOrder = cff.otFont.getGlyphOrder
39	topDictData = TopDictIndex(None, cff2GetGlyphOrder, None)
40	topDictData.items = cff.topDictIndex.items
41	cff.topDictIndex = topDictData
42	topDict = topDictData[0]
43	if hasattr(topDict, 'Private'):
44		privateDict = topDict.Private
45	else:
46		privateDict = None
47	opOrder = buildOrder(topDictOperators2)
48	topDict.order = opOrder
49	topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
50	if not hasattr(topDict, "FDArray"):
51		fdArray = topDict.FDArray = FDArrayIndex()
52		fdArray.strings = None
53		fdArray.GlobalSubrs = topDict.GlobalSubrs
54		topDict.GlobalSubrs.fdArray = fdArray
55		charStrings = topDict.CharStrings
56		if charStrings.charStringsAreIndexed:
57			charStrings.charStringsIndex.fdArray = fdArray
58		else:
59			charStrings.fdArray = fdArray
60		fontDict = FontDict()
61		fontDict.setCFF2(True)
62		fdArray.append(fontDict)
63		fontDict.Private = privateDict
64		privateOpOrder = buildOrder(privateDictOperators2)
65		for entry in privateDictOperators:
66			key = entry[1]
67			if key not in privateOpOrder:
68				if key in privateDict.rawDict:
69					# print "Removing private dict", key
70					del privateDict.rawDict[key]
71				if hasattr(privateDict, key):
72					delattr(privateDict, key)
73					# print "Removing privateDict attr", key
74	else:
75		# clean up the PrivateDicts in the fdArray
76		fdArray = topDict.FDArray
77		privateOpOrder = buildOrder(privateDictOperators2)
78		for fontDict in fdArray:
79			fontDict.setCFF2(True)
80			for key in list(fontDict.rawDict.keys()):
81				if key not in fontDict.order:
82					del fontDict.rawDict[key]
83					if hasattr(fontDict, key):
84						delattr(fontDict, key)
85
86			privateDict = fontDict.Private
87			for entry in privateDictOperators:
88				key = entry[1]
89				if key not in privateOpOrder:
90					if key in privateDict.rawDict:
91						# print "Removing private dict", key
92						del privateDict.rawDict[key]
93					if hasattr(privateDict, key):
94						delattr(privateDict, key)
95						# print "Removing privateDict attr", key
96	# Now delete up the decrecated topDict operators from CFF 1.0
97	for entry in topDictOperators:
98		key = entry[1]
99		if key not in opOrder:
100			if key in topDict.rawDict:
101				del topDict.rawDict[key]
102			if hasattr(topDict, key):
103				delattr(topDict, key)
104
105	# At this point, the Subrs and Charstrings are all still T2Charstring class
106	# easiest to fix this by compiling, then decompiling again
107	cff.major = 2
108	file = BytesIO()
109	cff.compile(file, otFont, isCFF2=True)
110	file.seek(0)
111	cff.decompile(file, otFont, isCFF2=True)
112
113
114def convertCFFtoCFF2(varFont):
115	# Convert base font to a single master CFF2 font.
116	cffTable = varFont['CFF ']
117	lib_convertCFFToCFF2(cffTable.cff, varFont)
118	newCFF2 = newTable("CFF2")
119	newCFF2.cff = cffTable.cff
120	varFont['CFF2'] = newCFF2
121	del varFont['CFF ']
122
123
124class MergeDictError(TypeError):
125	def __init__(self, key, value, values):
126		error_msg = ["For the Private Dict key '{}', ".format(key),
127					 "the default font value list:",
128					 "\t{}".format(value),
129					 "had a different number of values than a region font:"]
130		error_msg += ["\t{}".format(region_value) for region_value in values]
131		error_msg = os.linesep.join(error_msg)
132
133
134def conv_to_int(num):
135	if num % 1 == 0:
136		return int(num)
137	return num
138
139
140pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues",
141				   "FamilyOtherBlues", "BlueScale", "BlueShift",
142				   "BlueFuzz", "StdHW", "StdVW", "StemSnapH",
143				   "StemSnapV")
144
145
146def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model):
147	if hasattr(region_top_dicts[0], 'FDArray'):
148		regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts]
149	else:
150		regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts]
151	for fd_index, font_dict in enumerate(topDict.FDArray):
152		private_dict = font_dict.Private
153		pds = [private_dict] + [
154			regionFDArray[fd_index].Private for regionFDArray in regionFDArrays
155			]
156		for key, value in private_dict.rawDict.items():
157			if key not in pd_blend_fields:
158				continue
159			if isinstance(value, list):
160				try:
161					values = [pd.rawDict[key] for pd in pds]
162				except KeyError:
163					del private_dict.rawDict[key]
164					print(
165						b"Warning: {key} in default font Private dict is "
166						b"missing from another font, and was "
167						b"discarded.".format(key=key))
168					continue
169				try:
170					values = zip(*values)
171				except IndexError:
172					raise MergeDictError(key, value, values)
173				"""
174				Row 0 contains the first  value from each master.
175				Convert each row from absolute values to relative
176				values from the previous row.
177				e.g for three masters,	a list of values was:
178				master 0 OtherBlues = [-217,-205]
179				master 1 OtherBlues = [-234,-222]
180				master 1 OtherBlues = [-188,-176]
181				The call to zip() converts this to:
182				[(-217, -234, -188), (-205, -222, -176)]
183				and is converted finally to:
184				OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]]
185				"""
186				dataList = []
187				prev_val_list = [0] * num_masters
188				any_points_differ = False
189				for val_list in values:
190					rel_list = [(val - prev_val_list[i]) for (
191							i, val) in enumerate(val_list)]
192					if (not any_points_differ) and not allEqual(rel_list):
193						any_points_differ = True
194					prev_val_list = val_list
195					deltas = var_model.getDeltas(rel_list)
196					# Convert numbers with no decimal part to an int.
197					deltas = [conv_to_int(delta) for delta in deltas]
198					# For PrivateDict BlueValues, the default font
199					# values are absolute, not relative to the prior value.
200					deltas[0] = val_list[0]
201					dataList.append(deltas)
202				# If there are no blend values,then
203				# we can collapse the blend lists.
204				if not any_points_differ:
205					dataList = [data[0] for data in dataList]
206			else:
207				values = [pd.rawDict[key] for pd in pds]
208				if not allEqual(values):
209					dataList = var_model.getDeltas(values)
210				else:
211					dataList = values[0]
212			private_dict.rawDict[key] = dataList
213
214
215def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder):
216	topDict = varFont['CFF2'].cff.topDictIndex[0]
217	default_charstrings = topDict.CharStrings
218	region_fonts = ordered_fonts_list[1:]
219	region_top_dicts = [
220			ttFont['CFF '].cff.topDictIndex[0] for ttFont in region_fonts
221				]
222	num_masters = len(model.mapping)
223	merge_PrivateDicts(topDict, region_top_dicts, num_masters, model)
224	merge_charstrings(default_charstrings,
225					  glyphOrder,
226					  num_masters,
227					  region_top_dicts, model)
228
229
230def merge_charstrings(default_charstrings,
231					  glyphOrder,
232					  num_masters,
233					  region_top_dicts,
234					  var_model):
235	for gname in glyphOrder:
236		default_charstring = default_charstrings[gname]
237		var_pen = CFF2CharStringMergePen([], gname, num_masters, 0)
238		default_charstring.outlineExtractor = CFFToCFF2OutlineExtractor
239		default_charstring.draw(var_pen)
240		for region_idx, region_td in enumerate(region_top_dicts, start=1):
241			region_charstrings = region_td.CharStrings
242			region_charstring = region_charstrings[gname]
243			var_pen.restart(region_idx)
244			region_charstring.draw(var_pen)
245		new_charstring = var_pen.getCharString(
246			private=default_charstring.private,
247			globalSubrs=default_charstring.globalSubrs,
248			var_model=var_model, optimize=True)
249		default_charstrings[gname] = new_charstring
250
251
252class MergeTypeError(TypeError):
253	def __init__(self, point_type, pt_index, m_index, default_type, glyphName):
254		self.error_msg = [
255					"In glyph '{gname}' "
256					"'{point_type}' at point index {pt_index} in master "
257					"index {m_index} differs from the default font point "
258					"type '{default_type}'"
259					"".format(gname=glyphName,
260							  point_type=point_type, pt_index=pt_index,
261							  m_index=m_index, default_type=default_type)
262					][0]
263		super(MergeTypeError, self).__init__(self.error_msg)
264
265
266def makeRoundNumberFunc(tolerance):
267	if tolerance < 0:
268		raise ValueError("Rounding tolerance must be positive")
269
270	def roundNumber(val):
271		return t2c_round(val, tolerance)
272
273	return roundNumber
274
275
276class CFFToCFF2OutlineExtractor(T2OutlineExtractor):
277	""" This class is used to remove the initial width
278	from the CFF charstring without adding the width
279	to self.nominalWidthX, which is None.
280	"""
281	def popallWidth(self, evenOdd=0):
282		args = self.popall()
283		if not self.gotWidth:
284			if evenOdd ^ (len(args) % 2):
285				args = args[1:]
286			self.width = self.defaultWidthX
287			self.gotWidth = 1
288		return args
289
290
291class CFF2CharStringMergePen(T2CharStringPen):
292	"""Pen to merge Type 2 CharStrings.
293	"""
294	def __init__(self, default_commands,
295				 glyphName, num_masters, master_idx, roundTolerance=0.5):
296		super(
297			CFF2CharStringMergePen,
298			self).__init__(width=None,
299						   glyphSet=None, CFF2=True,
300						   roundTolerance=roundTolerance)
301		self.pt_index = 0
302		self._commands = default_commands
303		self.m_index = master_idx
304		self.num_masters = num_masters
305		self.prev_move_idx = 0
306		self.glyphName = glyphName
307		self.roundNumber = makeRoundNumberFunc(roundTolerance)
308
309	def _p(self, pt):
310		""" Unlike T2CharstringPen, this class stores absolute values.
311		This is to allow the logic in check_and_fix_closepath() to work,
312		where the current or previous absolute point has to be compared to
313		the path start-point.
314		"""
315		self._p0 = pt
316		return list(self._p0)
317
318	def add_point(self, point_type, pt_coords):
319		if self.m_index == 0:
320			self._commands.append([point_type, [pt_coords]])
321		else:
322			cmd = self._commands[self.pt_index]
323			if cmd[0] != point_type:
324				# Fix some issues that show up in some
325				# CFF workflows, even when fonts are
326				# topologically merge compatible.
327				success, pt_coords = self.check_and_fix_flat_curve(
328							cmd, point_type, pt_coords)
329				if not success:
330					success = self.check_and_fix_closepath(
331							cmd, point_type, pt_coords)
332					if success:
333						# We may have incremented self.pt_index
334						cmd = self._commands[self.pt_index]
335						if cmd[0] != point_type:
336							success = False
337					if not success:
338						raise MergeTypeError(point_type,
339											 self.pt_index, len(cmd[1]),
340											 cmd[0], self.glyphName)
341			cmd[1].append(pt_coords)
342		self.pt_index += 1
343
344	def _moveTo(self, pt):
345		pt_coords = self._p(pt)
346		self.add_point('rmoveto', pt_coords)
347		# I set prev_move_idx here because add_point()
348		# can change self.pt_index.
349		self.prev_move_idx = self.pt_index - 1
350
351	def _lineTo(self, pt):
352		pt_coords = self._p(pt)
353		self.add_point('rlineto', pt_coords)
354
355	def _curveToOne(self, pt1, pt2, pt3):
356		_p = self._p
357		pt_coords = _p(pt1)+_p(pt2)+_p(pt3)
358		self.add_point('rrcurveto', pt_coords)
359
360	def _closePath(self):
361		pass
362
363	def _endPath(self):
364		pass
365
366	def restart(self, region_idx):
367		self.pt_index = 0
368		self.m_index = region_idx
369		self._p0 = (0, 0)
370
371	def getCommands(self):
372		return self._commands
373
374	def reorder_blend_args(self, commands):
375		"""
376		We first re-order the master coordinate values.
377		For a moveto to lineto, the args are now arranged as:
378			[ [master_0 x,y], [master_1 x,y], [master_2 x,y] ]
379		We re-arrange this to
380		[	[master_0 x, master_1 x, master_2 x],
381			[master_0 y, master_1 y, master_2 y]
382		]
383		We also make the value relative.
384		If the master values are all the same, we collapse the list to
385		as single value instead of a list.
386		"""
387		for cmd in commands:
388			# arg[i] is the set of arguments for this operator from master i.
389			args = cmd[1]
390			m_args = zip(*args)
391			# m_args[n] is now all num_master args for the i'th argument
392			# for this operation.
393			cmd[1] = m_args
394
395		# Now convert from absolute to relative
396		x0 = [0]*self.num_masters
397		y0 = [0]*self.num_masters
398		for cmd in self._commands:
399			is_x = True
400			coords = cmd[1]
401			rel_coords = []
402			for coord in coords:
403				prev_coord = x0 if is_x else y0
404				rel_coord = [pt[0] - pt[1] for pt in zip(coord, prev_coord)]
405
406				if allEqual(rel_coord):
407					rel_coord = rel_coord[0]
408				rel_coords.append(rel_coord)
409				if is_x:
410					x0 = coord
411				else:
412					y0 = coord
413				is_x = not is_x
414			cmd[1] = rel_coords
415		return commands
416
417	@staticmethod
418	def mergeCommandsToProgram(commands, var_model, round_func):
419		"""
420		Takes a commands list as returned by programToCommands() and
421		converts it back to a T2CharString or CFF2Charstring program list. I
422		need to use this rather than specialize.commandsToProgram, as the
423		commands produced by CFF2CharStringMergePen initially contains a
424		list of coordinate values, one for each master, wherever a single
425		coordinate value is expected by the regular logic. The problem with
426		doing using the specialize.py functions is that a commands list is
427		expected to be a op name with its associated argument list. For the
428		commands list here, some of the arguments may need to be converted
429		to a new argument list and opcode.
430		This version will convert each list of master arguments to a blend
431		op and its arguments, and will also combine successive blend ops up
432		to the stack limit.
433		"""
434		program = []
435		for op, args in commands:
436			num_args = len(args)
437			# some of the args may be blend lists, and some may be
438			# single coordinate values.
439			i = 0
440			stack_use = 0
441			while i < num_args:
442				arg = args[i]
443				if not isinstance(arg, list):
444					program.append(arg)
445					i += 1
446					stack_use += 1
447				else:
448					prev_stack_use = stack_use
449					""" The arg is a tuple of blend values.
450					These are each (master 0,master 1..master n)
451					Combine as many successive tuples as we can,
452					up to the max stack limit.
453					"""
454					num_masters = len(arg)
455					blendlist = [arg]
456					i += 1
457					stack_use += 1 + num_masters  # 1 for the num_blends arg
458					while (i < num_args) and isinstance(args[i], list):
459						blendlist.append(args[i])
460						i += 1
461						stack_use += num_masters
462						if stack_use + num_masters > maxStackLimit:
463							# if we are here, max stack is is the CFF2 max stack.
464							break
465					num_blends = len(blendlist)
466					# append the 'num_blends' default font values
467					for arg in blendlist:
468						if round_func:
469							arg[0] = round_func(arg[0])
470						program.append(arg[0])
471					for arg in blendlist:
472						# for each coordinate tuple, append the region deltas
473						if len(arg) != 3:
474							print(arg)
475							import pdb
476							pdb.set_trace()
477						deltas = var_model.getDeltas(arg)
478						if round_func:
479							deltas = [round_func(delta) for delta in deltas]
480						# First item in 'deltas' is the default master value;
481						# for CFF2 data, that has already been written.
482						program.extend(deltas[1:])
483					program.append(num_blends)
484					program.append('blend')
485					stack_use = prev_stack_use + num_blends
486			if op:
487				program.append(op)
488		return program
489
490
491	def getCharString(self, private=None, globalSubrs=None,
492					  var_model=None, optimize=True):
493		commands = self._commands
494		commands = self.reorder_blend_args(commands)
495		if optimize:
496			commands = specializeCommands(commands, generalizeFirst=False,
497										  maxstack=maxStackLimit)
498		program = self.mergeCommandsToProgram(commands, var_model=var_model,
499									round_func=self.roundNumber)
500		charString = T2CharString(program=program, private=private,
501							  globalSubrs=globalSubrs)
502		return charString
503