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