Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/fontTools/pens/basePen.py: 30%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

189 statements  

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, Dict 

40 

41from fontTools.misc.loggingTools import LogMixin 

42from fontTools.misc.transform import DecomposedTransform, Identity 

43 

44__all__ = [ 

45 "AbstractPen", 

46 "NullPen", 

47 "BasePen", 

48 "PenError", 

49 "decomposeSuperBezierSegment", 

50 "decomposeQuadraticSegment", 

51] 

52 

53 

54class PenError(Exception): 

55 """Represents an error during penning.""" 

56 

57 

58class OpenContourError(PenError): 

59 pass 

60 

61 

62class AbstractPen: 

63 def moveTo(self, pt: Tuple[float, float]) -> None: 

64 """Begin a new sub path, set the current point to 'pt'. You must 

65 end each sub path with a call to pen.closePath() or pen.endPath(). 

66 """ 

67 raise NotImplementedError 

68 

69 def lineTo(self, pt: Tuple[float, float]) -> None: 

70 """Draw a straight line from the current point to 'pt'.""" 

71 raise NotImplementedError 

72 

73 def curveTo(self, *points: Tuple[float, float]) -> None: 

74 """Draw a cubic bezier with an arbitrary number of control points. 

75 

76 The last point specified is on-curve, all others are off-curve 

77 (control) points. If the number of control points is > 2, the 

78 segment is split into multiple bezier segments. This works 

79 like this: 

80 

81 Let n be the number of control points (which is the number of 

82 arguments to this call minus 1). If n==2, a plain vanilla cubic 

83 bezier is drawn. If n==1, we fall back to a quadratic segment and 

84 if n==0 we draw a straight line. It gets interesting when n>2: 

85 n-1 PostScript-style cubic segments will be drawn as if it were 

86 one curve. See decomposeSuperBezierSegment(). 

87 

88 The conversion algorithm used for n>2 is inspired by NURB 

89 splines, and is conceptually equivalent to the TrueType "implied 

90 points" principle. See also decomposeQuadraticSegment(). 

91 """ 

92 raise NotImplementedError 

93 

94 def qCurveTo(self, *points: Tuple[float, float]) -> None: 

95 """Draw a whole string of quadratic curve segments. 

96 

97 The last point specified is on-curve, all others are off-curve 

98 points. 

99 

100 This method implements TrueType-style curves, breaking up curves 

101 using 'implied points': between each two consequtive off-curve points, 

102 there is one implied point exactly in the middle between them. See 

103 also decomposeQuadraticSegment(). 

104 

105 The last argument (normally the on-curve point) may be None. 

106 This is to support contours that have NO on-curve points (a rarely 

107 seen feature of TrueType outlines). 

108 """ 

109 raise NotImplementedError 

110 

111 def closePath(self) -> None: 

112 """Close the current sub path. You must call either pen.closePath() 

113 or pen.endPath() after each sub path. 

114 """ 

115 pass 

116 

117 def endPath(self) -> None: 

118 """End the current sub path, but don't close it. You must call 

119 either pen.closePath() or pen.endPath() after each sub path. 

120 """ 

121 pass 

122 

123 def addComponent( 

124 self, 

125 glyphName: str, 

126 transformation: Tuple[float, float, float, float, float, float], 

127 ) -> None: 

128 """Add a sub glyph. The 'transformation' argument must be a 6-tuple 

129 containing an affine transformation, or a Transform object from the 

130 fontTools.misc.transform module. More precisely: it should be a 

131 sequence containing 6 numbers. 

132 """ 

133 raise NotImplementedError 

134 

135 def addVarComponent( 

136 self, 

137 glyphName: str, 

138 transformation: DecomposedTransform, 

139 location: Dict[str, float], 

140 ) -> None: 

141 """Add a VarComponent sub glyph. The 'transformation' argument 

142 must be a DecomposedTransform from the fontTools.misc.transform module, 

143 and the 'location' argument must be a dictionary mapping axis tags 

144 to their locations. 

145 """ 

146 # GlyphSet decomposes for us 

147 raise AttributeError 

148 

149 

150class NullPen(AbstractPen): 

151 """A pen that does nothing.""" 

152 

153 def moveTo(self, pt): 

154 pass 

155 

156 def lineTo(self, pt): 

157 pass 

158 

159 def curveTo(self, *points): 

160 pass 

161 

162 def qCurveTo(self, *points): 

163 pass 

164 

165 def closePath(self): 

166 pass 

167 

168 def endPath(self): 

169 pass 

170 

171 def addComponent(self, glyphName, transformation): 

172 pass 

173 

174 def addVarComponent(self, glyphName, transformation, location): 

175 pass 

176 

177 

178class LoggingPen(LogMixin, AbstractPen): 

179 """A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)""" 

180 

181 pass 

182 

183 

184class MissingComponentError(KeyError): 

185 """Indicates a component pointing to a non-existent glyph in the glyphset.""" 

186 

187 

188class DecomposingPen(LoggingPen): 

189 """Implements a 'addComponent' method that decomposes components 

190 (i.e. draws them onto self as simple contours). 

191 It can also be used as a mixin class (e.g. see ContourRecordingPen). 

192 

193 You must override moveTo, lineTo, curveTo and qCurveTo. You may 

194 additionally override closePath, endPath and addComponent. 

195 

196 By default a warning message is logged when a base glyph is missing; 

197 set the class variable ``skipMissingComponents`` to False if you want 

198 all instances of a sub-class to raise a :class:`MissingComponentError` 

199 exception by default. 

200 """ 

201 

202 skipMissingComponents = True 

203 # alias error for convenience 

204 MissingComponentError = MissingComponentError 

205 

206 def __init__( 

207 self, 

208 glyphSet, 

209 *args, 

210 skipMissingComponents=None, 

211 reverseFlipped=False, 

212 **kwargs, 

213 ): 

214 """Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced 

215 as components are looked up by their name. 

216 

217 If the optional 'reverseFlipped' argument is True, components whose transformation 

218 matrix has a negative determinant will be decomposed with a reversed path direction 

219 to compensate for the flip. 

220 

221 The optional 'skipMissingComponents' argument can be set to True/False to 

222 override the homonymous class attribute for a given pen instance. 

223 """ 

224 super(DecomposingPen, self).__init__(*args, **kwargs) 

225 self.glyphSet = glyphSet 

226 self.skipMissingComponents = ( 

227 self.__class__.skipMissingComponents 

228 if skipMissingComponents is None 

229 else skipMissingComponents 

230 ) 

231 self.reverseFlipped = reverseFlipped 

232 

233 def addComponent(self, glyphName, transformation): 

234 """Transform the points of the base glyph and draw it onto self.""" 

235 from fontTools.pens.transformPen import TransformPen 

236 

237 try: 

238 glyph = self.glyphSet[glyphName] 

239 except KeyError: 

240 if not self.skipMissingComponents: 

241 raise MissingComponentError(glyphName) 

242 self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName) 

243 else: 

244 pen = self 

245 if transformation != Identity: 

246 pen = TransformPen(pen, transformation) 

247 if self.reverseFlipped: 

248 # if the transformation has a negative determinant, it will 

249 # reverse the contour direction of the component 

250 a, b, c, d = transformation[:4] 

251 det = a * d - b * c 

252 if det < 0: 

253 from fontTools.pens.reverseContourPen import ReverseContourPen 

254 

255 pen = ReverseContourPen(pen) 

256 glyph.draw(pen) 

257 

258 def addVarComponent(self, glyphName, transformation, location): 

259 # GlyphSet decomposes for us 

260 raise AttributeError 

261 

262 

263class BasePen(DecomposingPen): 

264 """Base class for drawing pens. You must override _moveTo, _lineTo and 

265 _curveToOne. You may additionally override _closePath, _endPath, 

266 addComponent, addVarComponent, and/or _qCurveToOne. You should not 

267 override any other methods. 

268 """ 

269 

270 def __init__(self, glyphSet=None): 

271 super(BasePen, self).__init__(glyphSet) 

272 self.__currentPoint = None 

273 

274 # must override 

275 

276 def _moveTo(self, pt): 

277 raise NotImplementedError 

278 

279 def _lineTo(self, pt): 

280 raise NotImplementedError 

281 

282 def _curveToOne(self, pt1, pt2, pt3): 

283 raise NotImplementedError 

284 

285 # may override 

286 

287 def _closePath(self): 

288 pass 

289 

290 def _endPath(self): 

291 pass 

292 

293 def _qCurveToOne(self, pt1, pt2): 

294 """This method implements the basic quadratic curve type. The 

295 default implementation delegates the work to the cubic curve 

296 function. Optionally override with a native implementation. 

297 """ 

298 pt0x, pt0y = self.__currentPoint 

299 pt1x, pt1y = pt1 

300 pt2x, pt2y = pt2 

301 mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x) 

302 mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y) 

303 mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x) 

304 mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y) 

305 self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2) 

306 

307 # don't override 

308 

309 def _getCurrentPoint(self): 

310 """Return the current point. This is not part of the public 

311 interface, yet is useful for subclasses. 

312 """ 

313 return self.__currentPoint 

314 

315 def closePath(self): 

316 self._closePath() 

317 self.__currentPoint = None 

318 

319 def endPath(self): 

320 self._endPath() 

321 self.__currentPoint = None 

322 

323 def moveTo(self, pt): 

324 self._moveTo(pt) 

325 self.__currentPoint = pt 

326 

327 def lineTo(self, pt): 

328 self._lineTo(pt) 

329 self.__currentPoint = pt 

330 

331 def curveTo(self, *points): 

332 n = len(points) - 1 # 'n' is the number of control points 

333 assert n >= 0 

334 if n == 2: 

335 # The common case, we have exactly two BCP's, so this is a standard 

336 # cubic bezier. Even though decomposeSuperBezierSegment() handles 

337 # this case just fine, we special-case it anyway since it's so 

338 # common. 

339 self._curveToOne(*points) 

340 self.__currentPoint = points[-1] 

341 elif n > 2: 

342 # n is the number of control points; split curve into n-1 cubic 

343 # bezier segments. The algorithm used here is inspired by NURB 

344 # splines and the TrueType "implied point" principle, and ensures 

345 # the smoothest possible connection between two curve segments, 

346 # with no disruption in the curvature. It is practical since it 

347 # allows one to construct multiple bezier segments with a much 

348 # smaller amount of points. 

349 _curveToOne = self._curveToOne 

350 for pt1, pt2, pt3 in decomposeSuperBezierSegment(points): 

351 _curveToOne(pt1, pt2, pt3) 

352 self.__currentPoint = pt3 

353 elif n == 1: 

354 self.qCurveTo(*points) 

355 elif n == 0: 

356 self.lineTo(points[0]) 

357 else: 

358 raise AssertionError("can't get there from here") 

359 

360 def qCurveTo(self, *points): 

361 n = len(points) - 1 # 'n' is the number of control points 

362 assert n >= 0 

363 if points[-1] is None: 

364 # Special case for TrueType quadratics: it is possible to 

365 # define a contour with NO on-curve points. BasePen supports 

366 # this by allowing the final argument (the expected on-curve 

367 # point) to be None. We simulate the feature by making the implied 

368 # on-curve point between the last and the first off-curve points 

369 # explicit. 

370 x, y = points[-2] # last off-curve point 

371 nx, ny = points[0] # first off-curve point 

372 impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny)) 

373 self.__currentPoint = impliedStartPoint 

374 self._moveTo(impliedStartPoint) 

375 points = points[:-1] + (impliedStartPoint,) 

376 if n > 0: 

377 # Split the string of points into discrete quadratic curve 

378 # segments. Between any two consecutive off-curve points 

379 # there's an implied on-curve point exactly in the middle. 

380 # This is where the segment splits. 

381 _qCurveToOne = self._qCurveToOne 

382 for pt1, pt2 in decomposeQuadraticSegment(points): 

383 _qCurveToOne(pt1, pt2) 

384 self.__currentPoint = pt2 

385 else: 

386 self.lineTo(points[0]) 

387 

388 

389def decomposeSuperBezierSegment(points): 

390 """Split the SuperBezier described by 'points' into a list of regular 

391 bezier segments. The 'points' argument must be a sequence with length 

392 3 or greater, containing (x, y) coordinates. The last point is the 

393 destination on-curve point, the rest of the points are off-curve points. 

394 The start point should not be supplied. 

395 

396 This function returns a list of (pt1, pt2, pt3) tuples, which each 

397 specify a regular curveto-style bezier segment. 

398 """ 

399 n = len(points) - 1 

400 assert n > 1 

401 bezierSegments = [] 

402 pt1, pt2, pt3 = points[0], None, None 

403 for i in range(2, n + 1): 

404 # calculate points in between control points. 

405 nDivisions = min(i, 3, n - i + 2) 

406 for j in range(1, nDivisions): 

407 factor = j / nDivisions 

408 temp1 = points[i - 1] 

409 temp2 = points[i - 2] 

410 temp = ( 

411 temp2[0] + factor * (temp1[0] - temp2[0]), 

412 temp2[1] + factor * (temp1[1] - temp2[1]), 

413 ) 

414 if pt2 is None: 

415 pt2 = temp 

416 else: 

417 pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1])) 

418 bezierSegments.append((pt1, pt2, pt3)) 

419 pt1, pt2, pt3 = temp, None, None 

420 bezierSegments.append((pt1, points[-2], points[-1])) 

421 return bezierSegments 

422 

423 

424def decomposeQuadraticSegment(points): 

425 """Split the quadratic curve segment described by 'points' into a list 

426 of "atomic" quadratic segments. The 'points' argument must be a sequence 

427 with length 2 or greater, containing (x, y) coordinates. The last point 

428 is the destination on-curve point, the rest of the points are off-curve 

429 points. The start point should not be supplied. 

430 

431 This function returns a list of (pt1, pt2) tuples, which each specify a 

432 plain quadratic bezier segment. 

433 """ 

434 n = len(points) - 1 

435 assert n > 0 

436 quadSegments = [] 

437 for i in range(n - 1): 

438 x, y = points[i] 

439 nx, ny = points[i + 1] 

440 impliedPt = (0.5 * (x + nx), 0.5 * (y + ny)) 

441 quadSegments.append((points[i], impliedPt)) 

442 quadSegments.append((points[-2], points[-1])) 

443 return quadSegments 

444 

445 

446class _TestPen(BasePen): 

447 """Test class that prints PostScript to stdout.""" 

448 

449 def _moveTo(self, pt): 

450 print("%s %s moveto" % (pt[0], pt[1])) 

451 

452 def _lineTo(self, pt): 

453 print("%s %s lineto" % (pt[0], pt[1])) 

454 

455 def _curveToOne(self, bcp1, bcp2, pt): 

456 print( 

457 "%s %s %s %s %s %s curveto" 

458 % (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1]) 

459 ) 

460 

461 def _closePath(self): 

462 print("closepath") 

463 

464 

465if __name__ == "__main__": 

466 pen = _TestPen(None) 

467 pen.moveTo((0, 0)) 

468 pen.lineTo((0, 100)) 

469 pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0)) 

470 pen.closePath() 

471 

472 pen = _TestPen(None) 

473 # testing the "no on-curve point" scenario 

474 pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None) 

475 pen.closePath()