• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2=========
3PointPens
4=========
5
6Where **SegmentPens** have an intuitive approach to drawing
7(if you're familiar with postscript anyway), the **PointPen**
8is geared towards accessing all the data in the contours of
9the glyph. A PointPen has a very simple interface, it just
10steps through all the points in a call from glyph.drawPoints().
11This allows the caller to provide more data for each point.
12For instance, whether or not a point is smooth, and its name.
13"""
14from __future__ import absolute_import, unicode_literals
15from fontTools.pens.basePen import AbstractPen
16import math
17
18__all__ = [
19	"AbstractPointPen",
20	"BasePointToSegmentPen",
21	"PointToSegmentPen",
22	"SegmentToPointPen",
23	"GuessSmoothPointPen",
24	"ReverseContourPointPen",
25]
26
27
28class AbstractPointPen(object):
29	"""
30	Baseclass for all PointPens.
31	"""
32
33	def beginPath(self, identifier=None, **kwargs):
34		"""Start a new sub path."""
35		raise NotImplementedError
36
37	def endPath(self):
38		"""End the current sub path."""
39		raise NotImplementedError
40
41	def addPoint(self, pt, segmentType=None, smooth=False, name=None,
42				 identifier=None, **kwargs):
43		"""Add a point to the current sub path."""
44		raise NotImplementedError
45
46	def addComponent(self, baseGlyphName, transformation, identifier=None,
47					 **kwargs):
48		"""Add a sub glyph."""
49		raise NotImplementedError
50
51
52class BasePointToSegmentPen(AbstractPointPen):
53	"""
54	Base class for retrieving the outline in a segment-oriented
55	way. The PointPen protocol is simple yet also a little tricky,
56	so when you need an outline presented as segments but you have
57	as points, do use this base implementation as it properly takes
58	care of all the edge cases.
59	"""
60
61	def __init__(self):
62		self.currentPath = None
63
64	def beginPath(self, identifier=None, **kwargs):
65		assert self.currentPath is None
66		self.currentPath = []
67
68	def _flushContour(self, segments):
69		"""Override this method.
70
71		It will be called for each non-empty sub path with a list
72		of segments: the 'segments' argument.
73
74		The segments list contains tuples of length 2:
75			(segmentType, points)
76
77		segmentType is one of "move", "line", "curve" or "qcurve".
78		"move" may only occur as the first segment, and it signifies
79		an OPEN path. A CLOSED path does NOT start with a "move", in
80		fact it will not contain a "move" at ALL.
81
82		The 'points' field in the 2-tuple is a list of point info
83		tuples. The list has 1 or more items, a point tuple has
84		four items:
85			(point, smooth, name, kwargs)
86		'point' is an (x, y) coordinate pair.
87
88		For a closed path, the initial moveTo point is defined as
89		the last point of the last segment.
90
91		The 'points' list of "move" and "line" segments always contains
92		exactly one point tuple.
93		"""
94		raise NotImplementedError
95
96	def endPath(self):
97		assert self.currentPath is not None
98		points = self.currentPath
99		self.currentPath = None
100		if not points:
101			return
102		if len(points) == 1:
103			# Not much more we can do than output a single move segment.
104			pt, segmentType, smooth, name, kwargs = points[0]
105			segments = [("move", [(pt, smooth, name, kwargs)])]
106			self._flushContour(segments)
107			return
108		segments = []
109		if points[0][1] == "move":
110			# It's an open contour, insert a "move" segment for the first
111			# point and remove that first point from the point list.
112			pt, segmentType, smooth, name, kwargs = points[0]
113			segments.append(("move", [(pt, smooth, name, kwargs)]))
114			points.pop(0)
115		else:
116			# It's a closed contour. Locate the first on-curve point, and
117			# rotate the point list so that it _ends_ with an on-curve
118			# point.
119			firstOnCurve = None
120			for i in range(len(points)):
121				segmentType = points[i][1]
122				if segmentType is not None:
123					firstOnCurve = i
124					break
125			if firstOnCurve is None:
126				# Special case for quadratics: a contour with no on-curve
127				# points. Add a "None" point. (See also the Pen protocol's
128				# qCurveTo() method and fontTools.pens.basePen.py.)
129				points.append((None, "qcurve", None, None, None))
130			else:
131				points = points[firstOnCurve+1:] + points[:firstOnCurve+1]
132
133		currentSegment = []
134		for pt, segmentType, smooth, name, kwargs in points:
135			currentSegment.append((pt, smooth, name, kwargs))
136			if segmentType is None:
137				continue
138			segments.append((segmentType, currentSegment))
139			currentSegment = []
140
141		self._flushContour(segments)
142
143	def addPoint(self, pt, segmentType=None, smooth=False, name=None,
144				 identifier=None, **kwargs):
145		self.currentPath.append((pt, segmentType, smooth, name, kwargs))
146
147
148class PointToSegmentPen(BasePointToSegmentPen):
149	"""
150	Adapter class that converts the PointPen protocol to the
151	(Segment)Pen protocol.
152	"""
153
154	def __init__(self, segmentPen, outputImpliedClosingLine=False):
155		BasePointToSegmentPen.__init__(self)
156		self.pen = segmentPen
157		self.outputImpliedClosingLine = outputImpliedClosingLine
158
159	def _flushContour(self, segments):
160		assert len(segments) >= 1
161		pen = self.pen
162		if segments[0][0] == "move":
163			# It's an open path.
164			closed = False
165			points = segments[0][1]
166			assert len(points) == 1, "illegal move segment point count: %d" % len(points)
167			movePt, smooth, name, kwargs = points[0]
168			del segments[0]
169		else:
170			# It's a closed path, do a moveTo to the last
171			# point of the last segment.
172			closed = True
173			segmentType, points = segments[-1]
174			movePt, smooth, name, kwargs = points[-1]
175		if movePt is None:
176			# quad special case: a contour with no on-curve points contains
177			# one "qcurve" segment that ends with a point that's None. We
178			# must not output a moveTo() in that case.
179			pass
180		else:
181			pen.moveTo(movePt)
182		outputImpliedClosingLine = self.outputImpliedClosingLine
183		nSegments = len(segments)
184		for i in range(nSegments):
185			segmentType, points = segments[i]
186			points = [pt for pt, smooth, name, kwargs in points]
187			if segmentType == "line":
188				assert len(points) == 1, "illegal line segment point count: %d" % len(points)
189				pt = points[0]
190				if i + 1 != nSegments or outputImpliedClosingLine or not closed:
191					pen.lineTo(pt)
192			elif segmentType == "curve":
193				pen.curveTo(*points)
194			elif segmentType == "qcurve":
195				pen.qCurveTo(*points)
196			else:
197				assert 0, "illegal segmentType: %s" % segmentType
198		if closed:
199			pen.closePath()
200		else:
201			pen.endPath()
202
203	def addComponent(self, glyphName, transform, identifier=None, **kwargs):
204		del identifier  # unused
205		self.pen.addComponent(glyphName, transform)
206
207
208class SegmentToPointPen(AbstractPen):
209	"""
210	Adapter class that converts the (Segment)Pen protocol to the
211	PointPen protocol.
212	"""
213
214	def __init__(self, pointPen, guessSmooth=True):
215		if guessSmooth:
216			self.pen = GuessSmoothPointPen(pointPen)
217		else:
218			self.pen = pointPen
219		self.contour = None
220
221	def _flushContour(self):
222		pen = self.pen
223		pen.beginPath()
224		for pt, segmentType in self.contour:
225			pen.addPoint(pt, segmentType=segmentType)
226		pen.endPath()
227
228	def moveTo(self, pt):
229		self.contour = []
230		self.contour.append((pt, "move"))
231
232	def lineTo(self, pt):
233		self.contour.append((pt, "line"))
234
235	def curveTo(self, *pts):
236		for pt in pts[:-1]:
237			self.contour.append((pt, None))
238		self.contour.append((pts[-1], "curve"))
239
240	def qCurveTo(self, *pts):
241		if pts[-1] is None:
242			self.contour = []
243		for pt in pts[:-1]:
244			self.contour.append((pt, None))
245		if pts[-1] is not None:
246			self.contour.append((pts[-1], "qcurve"))
247
248	def closePath(self):
249		if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]:
250			self.contour[0] = self.contour[-1]
251			del self.contour[-1]
252		else:
253			# There's an implied line at the end, replace "move" with "line"
254			# for the first point
255			pt, tp = self.contour[0]
256			if tp == "move":
257				self.contour[0] = pt, "line"
258		self._flushContour()
259		self.contour = None
260
261	def endPath(self):
262		self._flushContour()
263		self.contour = None
264
265	def addComponent(self, glyphName, transform):
266		assert self.contour is None
267		self.pen.addComponent(glyphName, transform)
268
269
270class GuessSmoothPointPen(AbstractPointPen):
271	"""
272	Filtering PointPen that tries to determine whether an on-curve point
273	should be "smooth", ie. that it's a "tangent" point or a "curve" point.
274	"""
275
276	def __init__(self, outPen):
277		self._outPen = outPen
278		self._points = None
279
280	def _flushContour(self):
281		points = self._points
282		nPoints = len(points)
283		if not nPoints:
284			return
285		if points[0][1] == "move":
286			# Open path.
287			indices = range(1, nPoints - 1)
288		elif nPoints > 1:
289			# Closed path. To avoid having to mod the contour index, we
290			# simply abuse Python's negative index feature, and start at -1
291			indices = range(-1, nPoints - 1)
292		else:
293			# closed path containing 1 point (!), ignore.
294			indices = []
295		for i in indices:
296			pt, segmentType, dummy, name, kwargs = points[i]
297			if segmentType is None:
298				continue
299			prev = i - 1
300			next = i + 1
301			if points[prev][1] is not None and points[next][1] is not None:
302				continue
303			# At least one of our neighbors is an off-curve point
304			pt = points[i][0]
305			prevPt = points[prev][0]
306			nextPt = points[next][0]
307			if pt != prevPt and pt != nextPt:
308				dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1]
309				dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1]
310				a1 = math.atan2(dx1, dy1)
311				a2 = math.atan2(dx2, dy2)
312				if abs(a1 - a2) < 0.05:
313					points[i] = pt, segmentType, True, name, kwargs
314
315		for pt, segmentType, smooth, name, kwargs in points:
316			self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
317
318	def beginPath(self, identifier=None, **kwargs):
319		assert self._points is None
320		self._points = []
321		if identifier is not None:
322			kwargs["identifier"] = identifier
323		self._outPen.beginPath(**kwargs)
324
325	def endPath(self):
326		self._flushContour()
327		self._outPen.endPath()
328		self._points = None
329
330	def addPoint(self, pt, segmentType=None, smooth=False, name=None,
331				 identifier=None, **kwargs):
332		if identifier is not None:
333			kwargs["identifier"] = identifier
334		self._points.append((pt, segmentType, False, name, kwargs))
335
336	def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
337		assert self._points is None
338		if identifier is not None:
339			kwargs["identifier"] = identifier
340		self._outPen.addComponent(glyphName, transformation, **kwargs)
341
342
343class ReverseContourPointPen(AbstractPointPen):
344	"""
345	This is a PointPen that passes outline data to another PointPen, but
346	reversing the winding direction of all contours. Components are simply
347	passed through unchanged.
348
349	Closed contours are reversed in such a way that the first point remains
350	the first point.
351	"""
352
353	def __init__(self, outputPointPen):
354		self.pen = outputPointPen
355		# a place to store the points for the current sub path
356		self.currentContour = None
357
358	def _flushContour(self):
359		pen = self.pen
360		contour = self.currentContour
361		if not contour:
362			pen.beginPath(identifier=self.currentContourIdentifier)
363			pen.endPath()
364			return
365
366		closed = contour[0][1] != "move"
367		if not closed:
368			lastSegmentType = "move"
369		else:
370			# Remove the first point and insert it at the end. When
371			# the list of points gets reversed, this point will then
372			# again be at the start. In other words, the following
373			# will hold:
374			#   for N in range(len(originalContour)):
375			#       originalContour[N] == reversedContour[-N]
376			contour.append(contour.pop(0))
377			# Find the first on-curve point.
378			firstOnCurve = None
379			for i in range(len(contour)):
380				if contour[i][1] is not None:
381					firstOnCurve = i
382					break
383			if firstOnCurve is None:
384				# There are no on-curve points, be basically have to
385				# do nothing but contour.reverse().
386				lastSegmentType = None
387			else:
388				lastSegmentType = contour[firstOnCurve][1]
389
390		contour.reverse()
391		if not closed:
392			# Open paths must start with a move, so we simply dump
393			# all off-curve points leading up to the first on-curve.
394			while contour[0][1] is None:
395				contour.pop(0)
396		pen.beginPath(identifier=self.currentContourIdentifier)
397		for pt, nextSegmentType, smooth, name, kwargs in contour:
398			if nextSegmentType is not None:
399				segmentType = lastSegmentType
400				lastSegmentType = nextSegmentType
401			else:
402				segmentType = None
403			pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs)
404		pen.endPath()
405
406	def beginPath(self, identifier=None, **kwargs):
407		assert self.currentContour is None
408		self.currentContour = []
409		self.currentContourIdentifier = identifier
410		self.onCurve = []
411
412	def endPath(self):
413		assert self.currentContour is not None
414		self._flushContour()
415		self.currentContour = None
416
417	def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
418		self.currentContour.append((pt, segmentType, smooth, name, kwargs))
419
420	def addComponent(self, glyphName, transform, identifier=None, **kwargs):
421		assert self.currentContour is None
422		self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)
423