basePen.py revision 285d7b81d3a1d9d060864438580f05c2b44366ff
1"""fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
2
3The Pen Protocol
4
5A Pen is a kind of object that standardizes the way how to "draw" outlines:
6it is a middle man between an outline and a drawing. In other words:
7it is an abstraction for drawing outlines, making sure that outline objects
8don't need to know the details about how and where they're being drawn, and
9that drawings don't need to know the details of how outlines are stored.
10
11The most basic pattern is this:
12
13    outline.draw(pen)  # 'outline' draws itself onto 'pen'
14
15Pens can be used to render outlines to the screen, but also to construct
16new outlines. Eg. an outline object can be both a drawable object (it has a
17draw() method) as well as a pen itself: you *build* an outline using pen
18methods.
19
20The AbstractPen class defines the Pen protocol. It implements almost
21nothing (only no-op closePath() and endPath() methods), but is useful
22for documentation purposes. Subclassing it basically tells the reader:
23"this class implements the Pen protocol.". An examples of an AbstractPen
24subclass is fontTools.pens.transformPen.TransformPen.
25
26The BasePen class is a base implementation useful for pens that actually
27draw (for example a pen renders outlines using a native graphics engine).
28BasePen contains a lot of base functionality, making it very easy to build
29a pen that fully conforms to the pen protocol. Note that if you subclass
30BasePen, you _don't_ override moveTo(), lineTo(), etc., but _moveTo(),
31_lineTo(), etc. See the BasePen doc string for details. Examples of
32BasePen subclasses are fontTools.pens.boundsPen.BoundsPen and
33fontTools.pens.cocoaPen.CocoaPen.
34
35Coordinates are usually expressed as (x, y) tuples, but generally any
36sequence of length 2 will do.
37"""
38
39
40__all__ = ["AbstractPen", "NullPen", "BasePen",
41           "decomposeSuperBezierSegment", "decomposeQuadraticSegment"]
42
43
44class AbstractPen(object):
45
46	def moveTo(self, pt):
47		"""Begin a new sub path, set the current point to 'pt'. You must
48		end each sub path with a call to pen.closePath() or pen.endPath().
49		"""
50		raise NotImplementedError
51
52	def lineTo(self, pt):
53		"""Draw a straight line from the current point to 'pt'."""
54		raise NotImplementedError
55
56	def curveTo(self, *points):
57		"""Draw a cubic bezier with an arbitrary number of control points.
58
59		The last point specified is on-curve, all others are off-curve
60		(control) points. If the number of control points is > 2, the
61		segment is split into multiple bezier segments. This works
62		like this:
63
64		Let n be the number of control points (which is the number of
65		arguments to this call minus 1). If n==2, a plain vanilla cubic
66		bezier is drawn. If n==1, we fall back to a quadratic segment and
67		if n==0 we draw a straight line. It gets interesting when n>2:
68		n-1 PostScript-style cubic segments will be drawn as if it were
69		one curve. See decomposeSuperBezierSegment().
70
71		The conversion algorithm used for n>2 is inspired by NURB
72		splines, and is conceptually equivalent to the TrueType "implied
73		points" principle. See also decomposeQuadraticSegment().
74		"""
75		raise NotImplementedError
76
77	def qCurveTo(self, *points):
78		"""Draw a whole string of quadratic curve segments.
79
80		The last point specified is on-curve, all others are off-curve
81		points.
82
83		This method implements TrueType-style curves, breaking up curves
84		using 'implied points': between each two consequtive off-curve points,
85		there is one implied point exactly in the middle between them. See
86		also decomposeQuadraticSegment().
87
88		The last argument (normally the on-curve point) may be None.
89		This is to support contours that have NO on-curve points (a rarely
90		seen feature of TrueType outlines).
91		"""
92		raise NotImplementedError
93
94	def closePath(self):
95		"""Close the current sub path. You must call either pen.closePath()
96		or pen.endPath() after each sub path.
97		"""
98		pass
99
100	def endPath(self):
101		"""End the current sub path, but don't close it. You must call
102		either pen.closePath() or pen.endPath() after each sub path.
103		"""
104		pass
105
106	def addComponent(self, glyphName, transformation):
107		"""Add a sub glyph. The 'transformation' argument must be a 6-tuple
108		containing an affine transformation, or a Transform object from the
109		fontTools.misc.transform module. More precisely: it should be a
110		sequence containing 6 numbers.
111		"""
112		raise NotImplementedError
113
114
115class NullPen(object):
116
117	"""A pen that does nothing.
118	"""
119
120	def moveTo(self, pt):
121		pass
122
123	def lineTo(self, pt):
124		pass
125
126	def curveTo(self, *points):
127		pass
128
129	def qCurveTo(self, *points):
130		pass
131
132	def closePath(self):
133		pass
134
135	def endPath(self):
136		pass
137
138	def addComponent(self, glyphName, transformation):
139		pass
140
141
142class BasePen(AbstractPen):
143
144	"""Base class for drawing pens. You must override _moveTo, _lineTo and
145	_curveToOne. You may additionally override _closePath, _endPath,
146	addComponent and/or _qCurveToOne. You should not override any other
147	methods.
148	"""
149
150	def __init__(self, glyphSet):
151		self.glyphSet = glyphSet
152		self.__currentPoint = None
153
154	# must override
155
156	def _moveTo(self, pt):
157		raise NotImplementedError
158
159	def _lineTo(self, pt):
160		raise NotImplementedError
161
162	def _curveToOne(self, pt1, pt2, pt3):
163		raise NotImplementedError
164
165	# may override
166
167	def _closePath(self):
168		pass
169
170	def _endPath(self):
171		pass
172
173	def _qCurveToOne(self, pt1, pt2):
174		"""This method implements the basic quadratic curve type. The
175		default implementation delegates the work to the cubic curve
176		function. Optionally override with a native implementation.
177		"""
178		pt0x, pt0y = self.__currentPoint
179		pt1x, pt1y = pt1
180		pt2x, pt2y = pt2
181		mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
182		mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
183		mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
184		mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
185		self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
186
187	def addComponent(self, glyphName, transformation):
188		"""This default implementation simply transforms the points
189		of the base glyph and draws it onto self.
190		"""
191		from fontTools.pens.transformPen import TransformPen
192		try:
193			glyph = self.glyphSet[glyphName]
194		except KeyError:
195			pass
196		else:
197			tPen = TransformPen(self, transformation)
198			glyph.draw(tPen)
199
200	# don't override
201
202	def _getCurrentPoint(self):
203		"""Return the current point. This is not part of the public
204		interface, yet is useful for subclasses.
205		"""
206		return self.__currentPoint
207
208	def closePath(self):
209		self._closePath()
210		self.__currentPoint = None
211
212	def endPath(self):
213		self._endPath()
214		self.__currentPoint = None
215
216	def moveTo(self, pt):
217		self._moveTo(pt)
218		self.__currentPoint = pt
219
220	def lineTo(self, pt):
221		self._lineTo(pt)
222		self.__currentPoint = pt
223
224	def curveTo(self, *points):
225		n = len(points) - 1  # 'n' is the number of control points
226		assert n >= 0
227		if n == 2:
228			# The common case, we have exactly two BCP's, so this is a standard
229			# cubic bezier. Even though decomposeSuperBezierSegment() handles
230			# this case just fine, we special-case it anyway since it's so
231			# common.
232			self._curveToOne(*points)
233			self.__currentPoint = points[-1]
234		elif n > 2:
235			# n is the number of control points; split curve into n-1 cubic
236			# bezier segments. The algorithm used here is inspired by NURB
237			# splines and the TrueType "implied point" principle, and ensures
238			# the smoothest possible connection between two curve segments,
239			# with no disruption in the curvature. It is practical since it
240			# allows one to construct multiple bezier segments with a much
241			# smaller amount of points.
242			_curveToOne = self._curveToOne
243			for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
244				_curveToOne(pt1, pt2, pt3)
245				self.__currentPoint = pt3
246		elif n == 1:
247			self.qCurveTo(*points)
248		elif n == 0:
249			self.lineTo(points[0])
250		else:
251			raise AssertionError, "can't get there from here"
252
253	def qCurveTo(self, *points):
254		n = len(points) - 1  # 'n' is the number of control points
255		assert n >= 0
256		if points[-1] is None:
257			# Special case for TrueType quadratics: it is possible to
258			# define a contour with NO on-curve points. BasePen supports
259			# this by allowing the final argument (the expected on-curve
260			# point) to be None. We simulate the feature by making the implied
261			# on-curve point between the last and the first off-curve points
262			# explicit.
263			x, y = points[-2]  # last off-curve point
264			nx, ny = points[0] # first off-curve point
265			impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
266			self.__currentPoint = impliedStartPoint
267			self._moveTo(impliedStartPoint)
268			points = points[:-1] + (impliedStartPoint,)
269		if n > 0:
270			# Split the string of points into discrete quadratic curve
271			# segments. Between any two consecutive off-curve points
272			# there's an implied on-curve point exactly in the middle.
273			# This is where the segment splits.
274			_qCurveToOne = self._qCurveToOne
275			for pt1, pt2 in decomposeQuadraticSegment(points):
276				_qCurveToOne(pt1, pt2)
277				self.__currentPoint = pt2
278		else:
279			self.lineTo(points[0])
280
281
282def decomposeSuperBezierSegment(points):
283	"""Split the SuperBezier described by 'points' into a list of regular
284	bezier segments. The 'points' argument must be a sequence with length
285	3 or greater, containing (x, y) coordinates. The last point is the
286	destination on-curve point, the rest of the points are off-curve points.
287	The start point should not be supplied.
288
289	This function returns a list of (pt1, pt2, pt3) tuples, which each
290	specify a regular curveto-style bezier segment.
291	"""
292	n = len(points) - 1
293	assert n > 1
294	bezierSegments = []
295	pt1, pt2, pt3 = points[0], None, None
296	for i in range(2, n+1):
297		# calculate points in between control points.
298		nDivisions = min(i, 3, n-i+2)
299		d = float(nDivisions)
300		for j in range(1, nDivisions):
301			factor = j / d
302			temp1 = points[i-1]
303			temp2 = points[i-2]
304			temp = (temp2[0] + factor * (temp1[0] - temp2[0]),
305					temp2[1] + factor * (temp1[1] - temp2[1]))
306			if pt2 is None:
307				pt2 = temp
308			else:
309				pt3 = (0.5 * (pt2[0] + temp[0]),
310					   0.5 * (pt2[1] + temp[1]))
311				bezierSegments.append((pt1, pt2, pt3))
312				pt1, pt2, pt3 = temp, None, None
313	bezierSegments.append((pt1, points[-2], points[-1]))
314	return bezierSegments
315
316
317def decomposeQuadraticSegment(points):
318	"""Split the quadratic curve segment described by 'points' into a list
319	of "atomic" quadratic segments. The 'points' argument must be a sequence
320	with length 2 or greater, containing (x, y) coordinates. The last point
321	is the destination on-curve point, the rest of the points are off-curve
322	points. The start point should not be supplied.
323
324	This function returns a list of (pt1, pt2) tuples, which each specify a
325	plain quadratic bezier segment.
326	"""
327	n = len(points) - 1
328	assert n > 0
329	quadSegments = []
330	for i in range(n - 1):
331		x, y = points[i]
332		nx, ny = points[i+1]
333		impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
334		quadSegments.append((points[i], impliedPt))
335	quadSegments.append((points[-2], points[-1]))
336	return quadSegments
337
338
339class _TestPen(BasePen):
340	"""Test class that prints PostScript to stdout."""
341	def _moveTo(self, pt):
342		print "%s %s moveto" % (pt[0], pt[1])
343	def _lineTo(self, pt):
344		print "%s %s lineto" % (pt[0], pt[1])
345	def _curveToOne(self, bcp1, bcp2, pt):
346		print "%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1],
347				bcp2[0], bcp2[1], pt[0], pt[1])
348	def _closePath(self):
349		print "closepath"
350
351
352if __name__ == "__main__":
353	pen = _TestPen(None)
354	pen.moveTo((0, 0))
355	pen.lineTo((0, 100))
356	pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
357	pen.closePath()
358
359	pen = _TestPen(None)
360	# testing the "no on-curve point" scenario
361	pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
362	pen.closePath()
363