Home | History | Annotate | Download | only in pens
      1 """fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
      2 
      3 The Pen Protocol
      4 
      5 A Pen is a kind of object that standardizes the way how to "draw" outlines:
      6 it is a middle man between an outline and a drawing. In other words:
      7 it is an abstraction for drawing outlines, making sure that outline objects
      8 don't need to know the details about how and where they're being drawn, and
      9 that drawings don't need to know the details of how outlines are stored.
     10 
     11 The most basic pattern is this:
     12 
     13     outline.draw(pen)  # 'outline' draws itself onto 'pen'
     14 
     15 Pens can be used to render outlines to the screen, but also to construct
     16 new outlines. Eg. an outline object can be both a drawable object (it has a
     17 draw() method) as well as a pen itself: you *build* an outline using pen
     18 methods.
     19 
     20 The AbstractPen class defines the Pen protocol. It implements almost
     21 nothing (only no-op closePath() and endPath() methods), but is useful
     22 for documentation purposes. Subclassing it basically tells the reader:
     23 "this class implements the Pen protocol.". An examples of an AbstractPen
     24 subclass is fontTools.pens.transformPen.TransformPen.
     25 
     26 The BasePen class is a base implementation useful for pens that actually
     27 draw (for example a pen renders outlines using a native graphics engine).
     28 BasePen contains a lot of base functionality, making it very easy to build
     29 a pen that fully conforms to the pen protocol. Note that if you subclass
     30 BasePen, you _don't_ override moveTo(), lineTo(), etc., but _moveTo(),
     31 _lineTo(), etc. See the BasePen doc string for details. Examples of
     32 BasePen subclasses are fontTools.pens.boundsPen.BoundsPen and
     33 fontTools.pens.cocoaPen.CocoaPen.
     34 
     35 Coordinates are usually expressed as (x, y) tuples, but generally any
     36 sequence of length 2 will do.
     37 """
     38 
     39 from __future__ import print_function, division, absolute_import
     40 from fontTools.misc.py23 import *
     41 
     42 __all__ = ["AbstractPen", "NullPen", "BasePen",
     43            "decomposeSuperBezierSegment", "decomposeQuadraticSegment"]
     44 
     45 
     46 class AbstractPen(object):
     47 
     48 	def moveTo(self, pt):
     49 		"""Begin a new sub path, set the current point to 'pt'. You must
     50 		end each sub path with a call to pen.closePath() or pen.endPath().
     51 		"""
     52 		raise NotImplementedError
     53 
     54 	def lineTo(self, pt):
     55 		"""Draw a straight line from the current point to 'pt'."""
     56 		raise NotImplementedError
     57 
     58 	def curveTo(self, *points):
     59 		"""Draw a cubic bezier with an arbitrary number of control points.
     60 
     61 		The last point specified is on-curve, all others are off-curve
     62 		(control) points. If the number of control points is > 2, the
     63 		segment is split into multiple bezier segments. This works
     64 		like this:
     65 
     66 		Let n be the number of control points (which is the number of
     67 		arguments to this call minus 1). If n==2, a plain vanilla cubic
     68 		bezier is drawn. If n==1, we fall back to a quadratic segment and
     69 		if n==0 we draw a straight line. It gets interesting when n>2:
     70 		n-1 PostScript-style cubic segments will be drawn as if it were
     71 		one curve. See decomposeSuperBezierSegment().
     72 
     73 		The conversion algorithm used for n>2 is inspired by NURB
     74 		splines, and is conceptually equivalent to the TrueType "implied
     75 		points" principle. See also decomposeQuadraticSegment().
     76 		"""
     77 		raise NotImplementedError
     78 
     79 	def qCurveTo(self, *points):
     80 		"""Draw a whole string of quadratic curve segments.
     81 
     82 		The last point specified is on-curve, all others are off-curve
     83 		points.
     84 
     85 		This method implements TrueType-style curves, breaking up curves
     86 		using 'implied points': between each two consequtive off-curve points,
     87 		there is one implied point exactly in the middle between them. See
     88 		also decomposeQuadraticSegment().
     89 
     90 		The last argument (normally the on-curve point) may be None.
     91 		This is to support contours that have NO on-curve points (a rarely
     92 		seen feature of TrueType outlines).
     93 		"""
     94 		raise NotImplementedError
     95 
     96 	def closePath(self):
     97 		"""Close the current sub path. You must call either pen.closePath()
     98 		or pen.endPath() after each sub path.
     99 		"""
    100 		pass
    101 
    102 	def endPath(self):
    103 		"""End the current sub path, but don't close it. You must call
    104 		either pen.closePath() or pen.endPath() after each sub path.
    105 		"""
    106 		pass
    107 
    108 	def addComponent(self, glyphName, transformation):
    109 		"""Add a sub glyph. The 'transformation' argument must be a 6-tuple
    110 		containing an affine transformation, or a Transform object from the
    111 		fontTools.misc.transform module. More precisely: it should be a
    112 		sequence containing 6 numbers.
    113 		"""
    114 		raise NotImplementedError
    115 
    116 
    117 class NullPen(object):
    118 
    119 	"""A pen that does nothing.
    120 	"""
    121 
    122 	def moveTo(self, pt):
    123 		pass
    124 
    125 	def lineTo(self, pt):
    126 		pass
    127 
    128 	def curveTo(self, *points):
    129 		pass
    130 
    131 	def qCurveTo(self, *points):
    132 		pass
    133 
    134 	def closePath(self):
    135 		pass
    136 
    137 	def endPath(self):
    138 		pass
    139 
    140 	def addComponent(self, glyphName, transformation):
    141 		pass
    142 
    143 
    144 class BasePen(AbstractPen):
    145 
    146 	"""Base class for drawing pens. You must override _moveTo, _lineTo and
    147 	_curveToOne. You may additionally override _closePath, _endPath,
    148 	addComponent and/or _qCurveToOne. You should not override any other
    149 	methods.
    150 	"""
    151 
    152 	def __init__(self, glyphSet):
    153 		self.glyphSet = glyphSet
    154 		self.__currentPoint = None
    155 
    156 	# must override
    157 
    158 	def _moveTo(self, pt):
    159 		raise NotImplementedError
    160 
    161 	def _lineTo(self, pt):
    162 		raise NotImplementedError
    163 
    164 	def _curveToOne(self, pt1, pt2, pt3):
    165 		raise NotImplementedError
    166 
    167 	# may override
    168 
    169 	def _closePath(self):
    170 		pass
    171 
    172 	def _endPath(self):
    173 		pass
    174 
    175 	def _qCurveToOne(self, pt1, pt2):
    176 		"""This method implements the basic quadratic curve type. The
    177 		default implementation delegates the work to the cubic curve
    178 		function. Optionally override with a native implementation.
    179 		"""
    180 		pt0x, pt0y = self.__currentPoint
    181 		pt1x, pt1y = pt1
    182 		pt2x, pt2y = pt2
    183 		mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
    184 		mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
    185 		mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
    186 		mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
    187 		self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
    188 
    189 	def addComponent(self, glyphName, transformation):
    190 		"""This default implementation simply transforms the points
    191 		of the base glyph and draws it onto self.
    192 		"""
    193 		from fontTools.pens.transformPen import TransformPen
    194 		try:
    195 			glyph = self.glyphSet[glyphName]
    196 		except KeyError:
    197 			pass
    198 		else:
    199 			tPen = TransformPen(self, transformation)
    200 			glyph.draw(tPen)
    201 
    202 	# don't override
    203 
    204 	def _getCurrentPoint(self):
    205 		"""Return the current point. This is not part of the public
    206 		interface, yet is useful for subclasses.
    207 		"""
    208 		return self.__currentPoint
    209 
    210 	def closePath(self):
    211 		self._closePath()
    212 		self.__currentPoint = None
    213 
    214 	def endPath(self):
    215 		self._endPath()
    216 		self.__currentPoint = None
    217 
    218 	def moveTo(self, pt):
    219 		self._moveTo(pt)
    220 		self.__currentPoint = pt
    221 
    222 	def lineTo(self, pt):
    223 		self._lineTo(pt)
    224 		self.__currentPoint = pt
    225 
    226 	def curveTo(self, *points):
    227 		n = len(points) - 1  # 'n' is the number of control points
    228 		assert n >= 0
    229 		if n == 2:
    230 			# The common case, we have exactly two BCP's, so this is a standard
    231 			# cubic bezier. Even though decomposeSuperBezierSegment() handles
    232 			# this case just fine, we special-case it anyway since it's so
    233 			# common.
    234 			self._curveToOne(*points)
    235 			self.__currentPoint = points[-1]
    236 		elif n > 2:
    237 			# n is the number of control points; split curve into n-1 cubic
    238 			# bezier segments. The algorithm used here is inspired by NURB
    239 			# splines and the TrueType "implied point" principle, and ensures
    240 			# the smoothest possible connection between two curve segments,
    241 			# with no disruption in the curvature. It is practical since it
    242 			# allows one to construct multiple bezier segments with a much
    243 			# smaller amount of points.
    244 			_curveToOne = self._curveToOne
    245 			for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
    246 				_curveToOne(pt1, pt2, pt3)
    247 				self.__currentPoint = pt3
    248 		elif n == 1:
    249 			self.qCurveTo(*points)
    250 		elif n == 0:
    251 			self.lineTo(points[0])
    252 		else:
    253 			raise AssertionError("can't get there from here")
    254 
    255 	def qCurveTo(self, *points):
    256 		n = len(points) - 1  # 'n' is the number of control points
    257 		assert n >= 0
    258 		if points[-1] is None:
    259 			# Special case for TrueType quadratics: it is possible to
    260 			# define a contour with NO on-curve points. BasePen supports
    261 			# this by allowing the final argument (the expected on-curve
    262 			# point) to be None. We simulate the feature by making the implied
    263 			# on-curve point between the last and the first off-curve points
    264 			# explicit.
    265 			x, y = points[-2]  # last off-curve point
    266 			nx, ny = points[0] # first off-curve point
    267 			impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
    268 			self.__currentPoint = impliedStartPoint
    269 			self._moveTo(impliedStartPoint)
    270 			points = points[:-1] + (impliedStartPoint,)
    271 		if n > 0:
    272 			# Split the string of points into discrete quadratic curve
    273 			# segments. Between any two consecutive off-curve points
    274 			# there's an implied on-curve point exactly in the middle.
    275 			# This is where the segment splits.
    276 			_qCurveToOne = self._qCurveToOne
    277 			for pt1, pt2 in decomposeQuadraticSegment(points):
    278 				_qCurveToOne(pt1, pt2)
    279 				self.__currentPoint = pt2
    280 		else:
    281 			self.lineTo(points[0])
    282 
    283 
    284 def decomposeSuperBezierSegment(points):
    285 	"""Split the SuperBezier described by 'points' into a list of regular
    286 	bezier segments. The 'points' argument must be a sequence with length
    287 	3 or greater, containing (x, y) coordinates. The last point is the
    288 	destination on-curve point, the rest of the points are off-curve points.
    289 	The start point should not be supplied.
    290 
    291 	This function returns a list of (pt1, pt2, pt3) tuples, which each
    292 	specify a regular curveto-style bezier segment.
    293 	"""
    294 	n = len(points) - 1
    295 	assert n > 1
    296 	bezierSegments = []
    297 	pt1, pt2, pt3 = points[0], None, None
    298 	for i in range(2, n+1):
    299 		# calculate points in between control points.
    300 		nDivisions = min(i, 3, n-i+2)
    301 		for j in range(1, nDivisions):
    302 			factor = j / nDivisions
    303 			temp1 = points[i-1]
    304 			temp2 = points[i-2]
    305 			temp = (temp2[0] + factor * (temp1[0] - temp2[0]),
    306 					temp2[1] + factor * (temp1[1] - temp2[1]))
    307 			if pt2 is None:
    308 				pt2 = temp
    309 			else:
    310 				pt3 = (0.5 * (pt2[0] + temp[0]),
    311 					   0.5 * (pt2[1] + temp[1]))
    312 				bezierSegments.append((pt1, pt2, pt3))
    313 				pt1, pt2, pt3 = temp, None, None
    314 	bezierSegments.append((pt1, points[-2], points[-1]))
    315 	return bezierSegments
    316 
    317 
    318 def decomposeQuadraticSegment(points):
    319 	"""Split the quadratic curve segment described by 'points' into a list
    320 	of "atomic" quadratic segments. The 'points' argument must be a sequence
    321 	with length 2 or greater, containing (x, y) coordinates. The last point
    322 	is the destination on-curve point, the rest of the points are off-curve
    323 	points. The start point should not be supplied.
    324 
    325 	This function returns a list of (pt1, pt2) tuples, which each specify a
    326 	plain quadratic bezier segment.
    327 	"""
    328 	n = len(points) - 1
    329 	assert n > 0
    330 	quadSegments = []
    331 	for i in range(n - 1):
    332 		x, y = points[i]
    333 		nx, ny = points[i+1]
    334 		impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
    335 		quadSegments.append((points[i], impliedPt))
    336 	quadSegments.append((points[-2], points[-1]))
    337 	return quadSegments
    338 
    339 
    340 class _TestPen(BasePen):
    341 	"""Test class that prints PostScript to stdout."""
    342 	def _moveTo(self, pt):
    343 		print("%s %s moveto" % (pt[0], pt[1]))
    344 	def _lineTo(self, pt):
    345 		print("%s %s lineto" % (pt[0], pt[1]))
    346 	def _curveToOne(self, bcp1, bcp2, pt):
    347 		print("%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1],
    348 				bcp2[0], bcp2[1], pt[0], pt[1]))
    349 	def _closePath(self):
    350 		print("closepath")
    351 
    352 
    353 if __name__ == "__main__":
    354 	pen = _TestPen(None)
    355 	pen.moveTo((0, 0))
    356 	pen.lineTo((0, 100))
    357 	pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
    358 	pen.closePath()
    359 
    360 	pen = _TestPen(None)
    361 	# testing the "no on-curve point" scenario
    362 	pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
    363 	pen.closePath()
    364