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