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