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