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

178 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:33 +0000

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 

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 

152 """A pen that does nothing.""" 

153 

154 def moveTo(self, pt): 

155 pass 

156 

157 def lineTo(self, pt): 

158 pass 

159 

160 def curveTo(self, *points): 

161 pass 

162 

163 def qCurveTo(self, *points): 

164 pass 

165 

166 def closePath(self): 

167 pass 

168 

169 def endPath(self): 

170 pass 

171 

172 def addComponent(self, glyphName, transformation): 

173 pass 

174 

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

176 pass 

177 

178 

179class LoggingPen(LogMixin, AbstractPen): 

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

181 

182 pass 

183 

184 

185class MissingComponentError(KeyError): 

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

187 

188 

189class DecomposingPen(LoggingPen): 

190 

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

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

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

194 

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

196 additionally override closePath, endPath and addComponent. 

197 

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

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

200 to raise a :class:`MissingComponentError` exception. 

201 """ 

202 

203 skipMissingComponents = True 

204 

205 def __init__(self, glyphSet): 

206 """Takes a single 'glyphSet' argument (dict), in which the glyphs 

207 that are referenced as components are looked up by their name. 

208 """ 

209 super(DecomposingPen, self).__init__() 

210 self.glyphSet = glyphSet 

211 

212 def addComponent(self, glyphName, transformation): 

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

214 from fontTools.pens.transformPen import TransformPen 

215 

216 try: 

217 glyph = self.glyphSet[glyphName] 

218 except KeyError: 

219 if not self.skipMissingComponents: 

220 raise MissingComponentError(glyphName) 

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

222 else: 

223 tPen = TransformPen(self, transformation) 

224 glyph.draw(tPen) 

225 

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

227 # GlyphSet decomposes for us 

228 raise AttributeError 

229 

230 

231class BasePen(DecomposingPen): 

232 

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

234 _curveToOne. You may additionally override _closePath, _endPath, 

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

236 override any other methods. 

237 """ 

238 

239 def __init__(self, glyphSet=None): 

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

241 self.__currentPoint = None 

242 

243 # must override 

244 

245 def _moveTo(self, pt): 

246 raise NotImplementedError 

247 

248 def _lineTo(self, pt): 

249 raise NotImplementedError 

250 

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

252 raise NotImplementedError 

253 

254 # may override 

255 

256 def _closePath(self): 

257 pass 

258 

259 def _endPath(self): 

260 pass 

261 

262 def _qCurveToOne(self, pt1, pt2): 

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

264 default implementation delegates the work to the cubic curve 

265 function. Optionally override with a native implementation. 

266 """ 

267 pt0x, pt0y = self.__currentPoint 

268 pt1x, pt1y = pt1 

269 pt2x, pt2y = pt2 

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

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

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

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

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

275 

276 # don't override 

277 

278 def _getCurrentPoint(self): 

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

280 interface, yet is useful for subclasses. 

281 """ 

282 return self.__currentPoint 

283 

284 def closePath(self): 

285 self._closePath() 

286 self.__currentPoint = None 

287 

288 def endPath(self): 

289 self._endPath() 

290 self.__currentPoint = None 

291 

292 def moveTo(self, pt): 

293 self._moveTo(pt) 

294 self.__currentPoint = pt 

295 

296 def lineTo(self, pt): 

297 self._lineTo(pt) 

298 self.__currentPoint = pt 

299 

300 def curveTo(self, *points): 

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

302 assert n >= 0 

303 if n == 2: 

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

305 # cubic bezier. Even though decomposeSuperBezierSegment() handles 

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

307 # common. 

308 self._curveToOne(*points) 

309 self.__currentPoint = points[-1] 

310 elif n > 2: 

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

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

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

314 # the smoothest possible connection between two curve segments, 

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

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

317 # smaller amount of points. 

318 _curveToOne = self._curveToOne 

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

320 _curveToOne(pt1, pt2, pt3) 

321 self.__currentPoint = pt3 

322 elif n == 1: 

323 self.qCurveTo(*points) 

324 elif n == 0: 

325 self.lineTo(points[0]) 

326 else: 

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

328 

329 def qCurveTo(self, *points): 

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

331 assert n >= 0 

332 if points[-1] is None: 

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

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

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

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

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

338 # explicit. 

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

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

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

342 self.__currentPoint = impliedStartPoint 

343 self._moveTo(impliedStartPoint) 

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

345 if n > 0: 

346 # Split the string of points into discrete quadratic curve 

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

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

349 # This is where the segment splits. 

350 _qCurveToOne = self._qCurveToOne 

351 for pt1, pt2 in decomposeQuadraticSegment(points): 

352 _qCurveToOne(pt1, pt2) 

353 self.__currentPoint = pt2 

354 else: 

355 self.lineTo(points[0]) 

356 

357 

358def decomposeSuperBezierSegment(points): 

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

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

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

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

363 The start point should not be supplied. 

364 

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

366 specify a regular curveto-style bezier segment. 

367 """ 

368 n = len(points) - 1 

369 assert n > 1 

370 bezierSegments = [] 

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

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

373 # calculate points in between control points. 

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

375 for j in range(1, nDivisions): 

376 factor = j / nDivisions 

377 temp1 = points[i - 1] 

378 temp2 = points[i - 2] 

379 temp = ( 

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

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

382 ) 

383 if pt2 is None: 

384 pt2 = temp 

385 else: 

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

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

388 pt1, pt2, pt3 = temp, None, None 

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

390 return bezierSegments 

391 

392 

393def decomposeQuadraticSegment(points): 

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

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

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

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

398 points. The start point should not be supplied. 

399 

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

401 plain quadratic bezier segment. 

402 """ 

403 n = len(points) - 1 

404 assert n > 0 

405 quadSegments = [] 

406 for i in range(n - 1): 

407 x, y = points[i] 

408 nx, ny = points[i + 1] 

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

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

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

412 return quadSegments 

413 

414 

415class _TestPen(BasePen): 

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

417 

418 def _moveTo(self, pt): 

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

420 

421 def _lineTo(self, pt): 

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

423 

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

425 print( 

426 "%s %s %s %s %s %s curveto" 

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

428 ) 

429 

430 def _closePath(self): 

431 print("closepath") 

432 

433 

434if __name__ == "__main__": 

435 pen = _TestPen(None) 

436 pen.moveTo((0, 0)) 

437 pen.lineTo((0, 100)) 

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

439 pen.closePath() 

440 

441 pen = _TestPen(None) 

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

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

444 pen.closePath()