basePen.py revision 40cde70f1640b8a25655fba4ee3ce7a9d5ca962e
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", "BasePen"]
41
42
43class AbstractPen:
44
45	def moveTo(self, pt):
46		"""Begin a new sub path, set the current point to 'pt'. You must
47		end each sub path with a call to pen.closePath() or pen.endPath().
48		"""
49		raise NotImplementedError
50
51	def lineTo(self, pt):
52		"""Draw a straight line from the current point to 'pt'."""
53		raise NotImplementedError
54
55	def curveTo(self, *points):
56		"""Draw a cubic bezier with an arbitrary number of control points.
57
58		The last point specified is on-curve, all others are off-curve
59		(control) points. If the number of control points is > 2, the
60		segment is split into multiple bezier segments. This works
61		like this:
62
63		Let n be the number of control points (which is the number of
64		arguments to this call minus 1). If n==2, a plain vanilla cubic
65		bezier is drawn. If n==1, we fall back to a quadratic segment and
66		if n==0 we draw a straight line. It gets interesting when n>2:
67		n-1 PostScript-style cubic segments will be drawn as if it were
68		one curve.
69
70		The conversion algorithm used for n>2 is inspired by NURB
71		splines, and is conceptually equivalent to the TrueType "implied
72		points" principle. See also qCurveTo().
73		"""
74		raise NotImplementedError
75
76	def qCurveTo(self, *points):
77		"""Draw a whole string of quadratic curve segments.
78
79		The last point specified is on-curve, all others are off-curve
80		points.
81
82		This method implements TrueType-style curves, breaking up curves
83		using 'implied points': between each two consequtive off-curve points,
84		there is one implied point exactly in the middle between them.
85
86		The last argument (normally the on-curve point) may be None.
87		This is to support contours that have NO on-curve points (a rarely
88		seen feature of TrueType outlines).
89		"""
90		raise NotImplementedError
91
92	def closePath(self):
93		"""Close the current sub path. You must call either pen.closePath()
94		or pen.endPath() after each sub path.
95		"""
96		pass
97
98	def endPath(self):
99		"""End the current sub path, but don't close it. You must call
100		either pen.closePath() or pen.endPath() after each sub path.
101		"""
102		pass
103
104	def addComponent(self, glyphName, transformation):
105		"""Add a sub glyph. The 'transformation' argument must be a 6-tuple
106		containing an affine transformation, or a Transform object from the
107		fontTools.misc.transform module. More precisely: it should be a
108		sequence containing 6 numbers.
109		"""
110		raise NotImplementedError
111
112
113class BasePen(AbstractPen):
114
115	"""Base class for drawing pens. You must override _moveTo, _lineTo and
116	_curveToOne. You may additionally override _closePath, _endPath,
117	addComponent and/or _qCurveToOne. You should not override any other
118	methods.
119	"""
120
121	def __init__(self, glyphSet):
122		self.glyphSet = glyphSet
123		self.__currentPoint = None
124
125	# must override
126
127	def _moveTo(self, pt):
128		raise NotImplementedError
129
130	def _lineTo(self, pt):
131		raise NotImplementedError
132
133	def _curveToOne(self, pt1, pt2, pt3):
134		raise NotImplementedError
135
136	# may override
137
138	def _closePath(self):
139		pass
140
141	def _endPath(self):
142		pass
143
144	def _qCurveToOne(self, pt1, pt2):
145		"""This method implements the basic quadratic curve type. The
146		default implementation delegates the work to the cubic curve
147		function. Optionally override with a native implementation.
148		"""
149		pt0x, pt0y = self.__currentPoint
150		pt1x, pt1y = pt1
151		pt2x, pt2y = pt2
152		mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
153		mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
154		mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
155		mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
156		self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
157
158	def addComponent(self, glyphName, transformation):
159		"""This default implementation simply transforms the points
160		of the base glyph and draws it onto self.
161		"""
162		from fontTools.pens.transformPen import TransformPen
163		tPen = TransformPen(self, transformation)
164		self.glyphSet[glyphName].draw(tPen)
165
166	# don't override
167
168	def _getCurrentPoint(self):
169		"""Return the current point. This is not part of the public
170		interface, yet is useful for subclasses.
171		"""
172		return self.__currentPoint
173
174	def closePath(self):
175		self._closePath()
176		self.__currentPoint = None
177
178	def endPath(self):
179		self._endPath()
180		self.__currentPoint = None
181
182	def moveTo(self, pt):
183		self._moveTo(pt)
184		self.__currentPoint = pt
185
186	def lineTo(self, pt):
187		self._lineTo(pt)
188		self.__currentPoint = pt
189
190	def curveTo(self, *points):
191		n = len(points) - 1  # 'n' is the number of control points
192		assert n >= 0
193		if n == 2:
194			# The common case, we have exactly two BCP's, so this is a standard
195			# cubic bezier.
196			self._curveToOne(*points)
197			self.__currentPoint = points[-1]
198		elif n > 2:
199			# n is the number of control points; split curve into n-1 cubic
200			# bezier segments. The algorithm used here is inspired by NURB
201			# splines and the TrueType "implied point" principle, and ensures
202			# the smoothest possible connection between two curve segments,
203			# with no disruption in the curvature. It is practical since it
204			# allows one to construct multiple bezier segments with a much
205			# smaller amount of points.
206			pt1, pt2, pt3 = points[0], None, None
207			for i in range(2, n+1):
208				# calculate points in between control points.
209				nDivisions = min(i, 3, n-i+2)
210				d = float(nDivisions)
211				for j in range(1, nDivisions):
212					factor = j / d
213					temp1 = points[i-1]
214					temp2 = points[i-2]
215					temp = (temp2[0] + factor * (temp1[0] - temp2[0]),
216					        temp2[1] + factor * (temp1[1] - temp2[1]))
217					if pt2 is None:
218						pt2 = temp
219					else:
220						pt3 = (0.5 * (pt2[0] + temp[0]),
221						       0.5 * (pt2[1] + temp[1]))
222						self._curveToOne(pt1, pt2, pt3)
223						self.__currentPoint = pt3
224						pt1, pt2, pt3 = temp, None, None
225			self._curveToOne(pt1, points[-2], points[-1])
226			self.__currentPoint = points[-1]
227		elif n == 1:
228			self.qCurveTo(*points)
229		elif n == 0:
230			self.lineTo(points[0])
231		else:
232			raise AssertionError, "can't get there from here"
233
234	def qCurveTo(self, *points):
235		n = len(points) - 1  # 'n' is the number of control points
236		assert n >= 0
237		if points[-1] is None:
238			# Special case for TrueType quadratics: it is possible to
239			# define a contour with NO on-curve points. BasePen supports
240			# this by allowing the final argument (the expected on-curve
241			# point) to be None. We simulate the feature by making the implied
242			# on-curve point between the last and the first off-curve points
243			# explicit.
244			x, y = points[-2]  # last off-curve point
245			nx, ny = points[0] # first off-curve point
246			impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
247			self.__currentPoint = impliedStartPoint
248			self._moveTo(impliedStartPoint)
249			points = points[:-1] + (impliedStartPoint,)
250		if n > 0:
251			# Split the string of points into discrete quadratic curve
252			# segments. Between any two consecutive off-curve points
253			# there's an implied on-curve point exactly in the middle.
254			# This is where the segment splits.
255			_qCurveToOne = self._qCurveToOne
256			for i in range(n - 1):
257				x, y = points[i]
258				nx, ny = points[i+1]
259				impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
260				_qCurveToOne(points[i], impliedPt)
261				self.__currentPoint = impliedPt
262			_qCurveToOne(points[-2], points[-1])
263			self.__currentPoint = points[-1]
264		else:
265			self.lineTo(points[0])
266
267
268class _TestPen(BasePen):
269	"""Test class that prints PostScript to stdout."""
270	def _moveTo(self, pt):
271		print "%s %s moveto" % (pt[0], pt[1])
272	def _lineTo(self, pt):
273		print "%s %s lineto" % (pt[0], pt[1])
274	def _curveToOne(self, bcp1, bcp2, pt):
275		print "%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1],
276				bcp2[0], bcp2[1], pt[0], pt[1])
277	def _closePath(self):
278		print "closepath"
279
280
281if __name__ == "__main__":
282	pen = _TestPen(None)
283	pen.moveTo((0, 0))
284	pen.lineTo((0, 100))
285	pen.qCurveTo((50, 75), (60, 50), (50, 25), (0, 0))
286	pen.closePath()
287
288	pen = _TestPen(None)
289	# testing the "no on-curve point" scenario
290	pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
291	pen.closePath()
292