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