Home | History | Annotate | Download | only in pens
      1 """
      2 =========
      3 PointPens
      4 =========
      5 
      6 Where **SegmentPens** have an intuitive approach to drawing
      7 (if you're familiar with postscript anyway), the **PointPen**
      8 is geared towards accessing all the data in the contours of
      9 the glyph. A PointPen has a very simple interface, it just
     10 steps through all the points in a call from glyph.drawPoints().
     11 This allows the caller to provide more data for each point.
     12 For instance, whether or not a point is smooth, and its name.
     13 """
     14 from __future__ import absolute_import, unicode_literals
     15 from fontTools.pens.basePen import AbstractPen
     16 import math
     17 
     18 __all__ = [
     19 	"AbstractPointPen",
     20 	"BasePointToSegmentPen",
     21 	"PointToSegmentPen",
     22 	"SegmentToPointPen",
     23 	"GuessSmoothPointPen",
     24 	"ReverseContourPointPen",
     25 ]
     26 
     27 
     28 class 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 
     52 class 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 
    148 class 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 
    208 class 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 
    270 class 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 
    343 class 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