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

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

318 statements  

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""" 

14 

15import math 

16from typing import Any, Optional, Tuple, Dict 

17 

18from fontTools.misc.loggingTools import LogMixin 

19from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError 

20from fontTools.misc.transform import DecomposedTransform, Identity 

21 

22__all__ = [ 

23 "AbstractPointPen", 

24 "BasePointToSegmentPen", 

25 "PointToSegmentPen", 

26 "SegmentToPointPen", 

27 "GuessSmoothPointPen", 

28 "ReverseContourPointPen", 

29] 

30 

31 

32class AbstractPointPen: 

33 """Baseclass for all PointPens.""" 

34 

35 def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None: 

36 """Start a new sub path.""" 

37 raise NotImplementedError 

38 

39 def endPath(self) -> None: 

40 """End the current sub path.""" 

41 raise NotImplementedError 

42 

43 def addPoint( 

44 self, 

45 pt: Tuple[float, float], 

46 segmentType: Optional[str] = None, 

47 smooth: bool = False, 

48 name: Optional[str] = None, 

49 identifier: Optional[str] = None, 

50 **kwargs: Any, 

51 ) -> None: 

52 """Add a point to the current sub path.""" 

53 raise NotImplementedError 

54 

55 def addComponent( 

56 self, 

57 baseGlyphName: str, 

58 transformation: Tuple[float, float, float, float, float, float], 

59 identifier: Optional[str] = None, 

60 **kwargs: Any, 

61 ) -> None: 

62 """Add a sub glyph.""" 

63 raise NotImplementedError 

64 

65 def addVarComponent( 

66 self, 

67 glyphName: str, 

68 transformation: DecomposedTransform, 

69 location: Dict[str, float], 

70 identifier: Optional[str] = None, 

71 **kwargs: Any, 

72 ) -> None: 

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

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

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

76 to their locations. 

77 """ 

78 # ttGlyphSet decomposes for us 

79 raise AttributeError 

80 

81 

82class BasePointToSegmentPen(AbstractPointPen): 

83 """ 

84 Base class for retrieving the outline in a segment-oriented 

85 way. The PointPen protocol is simple yet also a little tricky, 

86 so when you need an outline presented as segments but you have 

87 as points, do use this base implementation as it properly takes 

88 care of all the edge cases. 

89 """ 

90 

91 def __init__(self): 

92 self.currentPath = None 

93 

94 def beginPath(self, identifier=None, **kwargs): 

95 if self.currentPath is not None: 

96 raise PenError("Path already begun.") 

97 self.currentPath = [] 

98 

99 def _flushContour(self, segments): 

100 """Override this method. 

101 

102 It will be called for each non-empty sub path with a list 

103 of segments: the 'segments' argument. 

104 

105 The segments list contains tuples of length 2: 

106 (segmentType, points) 

107 

108 segmentType is one of "move", "line", "curve" or "qcurve". 

109 "move" may only occur as the first segment, and it signifies 

110 an OPEN path. A CLOSED path does NOT start with a "move", in 

111 fact it will not contain a "move" at ALL. 

112 

113 The 'points' field in the 2-tuple is a list of point info 

114 tuples. The list has 1 or more items, a point tuple has 

115 four items: 

116 (point, smooth, name, kwargs) 

117 'point' is an (x, y) coordinate pair. 

118 

119 For a closed path, the initial moveTo point is defined as 

120 the last point of the last segment. 

121 

122 The 'points' list of "move" and "line" segments always contains 

123 exactly one point tuple. 

124 """ 

125 raise NotImplementedError 

126 

127 def endPath(self): 

128 if self.currentPath is None: 

129 raise PenError("Path not begun.") 

130 points = self.currentPath 

131 self.currentPath = None 

132 if not points: 

133 return 

134 if len(points) == 1: 

135 # Not much more we can do than output a single move segment. 

136 pt, segmentType, smooth, name, kwargs = points[0] 

137 segments = [("move", [(pt, smooth, name, kwargs)])] 

138 self._flushContour(segments) 

139 return 

140 segments = [] 

141 if points[0][1] == "move": 

142 # It's an open contour, insert a "move" segment for the first 

143 # point and remove that first point from the point list. 

144 pt, segmentType, smooth, name, kwargs = points[0] 

145 segments.append(("move", [(pt, smooth, name, kwargs)])) 

146 points.pop(0) 

147 else: 

148 # It's a closed contour. Locate the first on-curve point, and 

149 # rotate the point list so that it _ends_ with an on-curve 

150 # point. 

151 firstOnCurve = None 

152 for i in range(len(points)): 

153 segmentType = points[i][1] 

154 if segmentType is not None: 

155 firstOnCurve = i 

156 break 

157 if firstOnCurve is None: 

158 # Special case for quadratics: a contour with no on-curve 

159 # points. Add a "None" point. (See also the Pen protocol's 

160 # qCurveTo() method and fontTools.pens.basePen.py.) 

161 points.append((None, "qcurve", None, None, None)) 

162 else: 

163 points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1] 

164 

165 currentSegment = [] 

166 for pt, segmentType, smooth, name, kwargs in points: 

167 currentSegment.append((pt, smooth, name, kwargs)) 

168 if segmentType is None: 

169 continue 

170 segments.append((segmentType, currentSegment)) 

171 currentSegment = [] 

172 

173 self._flushContour(segments) 

174 

175 def addPoint( 

176 self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs 

177 ): 

178 if self.currentPath is None: 

179 raise PenError("Path not begun") 

180 self.currentPath.append((pt, segmentType, smooth, name, kwargs)) 

181 

182 

183class PointToSegmentPen(BasePointToSegmentPen): 

184 """ 

185 Adapter class that converts the PointPen protocol to the 

186 (Segment)Pen protocol. 

187 

188 NOTE: The segment pen does not support and will drop point names, identifiers 

189 and kwargs. 

190 """ 

191 

192 def __init__(self, segmentPen, outputImpliedClosingLine=False): 

193 BasePointToSegmentPen.__init__(self) 

194 self.pen = segmentPen 

195 self.outputImpliedClosingLine = outputImpliedClosingLine 

196 

197 def _flushContour(self, segments): 

198 if not segments: 

199 raise PenError("Must have at least one segment.") 

200 pen = self.pen 

201 if segments[0][0] == "move": 

202 # It's an open path. 

203 closed = False 

204 points = segments[0][1] 

205 if len(points) != 1: 

206 raise PenError(f"Illegal move segment point count: {len(points)}") 

207 movePt, _, _, _ = points[0] 

208 del segments[0] 

209 else: 

210 # It's a closed path, do a moveTo to the last 

211 # point of the last segment. 

212 closed = True 

213 segmentType, points = segments[-1] 

214 movePt, _, _, _ = points[-1] 

215 if movePt is None: 

216 # quad special case: a contour with no on-curve points contains 

217 # one "qcurve" segment that ends with a point that's None. We 

218 # must not output a moveTo() in that case. 

219 pass 

220 else: 

221 pen.moveTo(movePt) 

222 outputImpliedClosingLine = self.outputImpliedClosingLine 

223 nSegments = len(segments) 

224 lastPt = movePt 

225 for i in range(nSegments): 

226 segmentType, points = segments[i] 

227 points = [pt for pt, _, _, _ in points] 

228 if segmentType == "line": 

229 if len(points) != 1: 

230 raise PenError(f"Illegal line segment point count: {len(points)}") 

231 pt = points[0] 

232 # For closed contours, a 'lineTo' is always implied from the last oncurve 

233 # point to the starting point, thus we can omit it when the last and 

234 # starting point don't overlap. 

235 # However, when the last oncurve point is a "line" segment and has same 

236 # coordinates as the starting point of a closed contour, we need to output 

237 # the closing 'lineTo' explicitly (regardless of the value of the 

238 # 'outputImpliedClosingLine' option) in order to disambiguate this case from 

239 # the implied closing 'lineTo', otherwise the duplicate point would be lost. 

240 # See https://github.com/googlefonts/fontmake/issues/572. 

241 if ( 

242 i + 1 != nSegments 

243 or outputImpliedClosingLine 

244 or not closed 

245 or pt == lastPt 

246 ): 

247 pen.lineTo(pt) 

248 lastPt = pt 

249 elif segmentType == "curve": 

250 pen.curveTo(*points) 

251 lastPt = points[-1] 

252 elif segmentType == "qcurve": 

253 pen.qCurveTo(*points) 

254 lastPt = points[-1] 

255 else: 

256 raise PenError(f"Illegal segmentType: {segmentType}") 

257 if closed: 

258 pen.closePath() 

259 else: 

260 pen.endPath() 

261 

262 def addComponent(self, glyphName, transform, identifier=None, **kwargs): 

263 del identifier # unused 

264 del kwargs # unused 

265 self.pen.addComponent(glyphName, transform) 

266 

267 

268class SegmentToPointPen(AbstractPen): 

269 """ 

270 Adapter class that converts the (Segment)Pen protocol to the 

271 PointPen protocol. 

272 """ 

273 

274 def __init__(self, pointPen, guessSmooth=True): 

275 if guessSmooth: 

276 self.pen = GuessSmoothPointPen(pointPen) 

277 else: 

278 self.pen = pointPen 

279 self.contour = None 

280 

281 def _flushContour(self): 

282 pen = self.pen 

283 pen.beginPath() 

284 for pt, segmentType in self.contour: 

285 pen.addPoint(pt, segmentType=segmentType) 

286 pen.endPath() 

287 

288 def moveTo(self, pt): 

289 self.contour = [] 

290 self.contour.append((pt, "move")) 

291 

292 def lineTo(self, pt): 

293 if self.contour is None: 

294 raise PenError("Contour missing required initial moveTo") 

295 self.contour.append((pt, "line")) 

296 

297 def curveTo(self, *pts): 

298 if not pts: 

299 raise TypeError("Must pass in at least one point") 

300 if self.contour is None: 

301 raise PenError("Contour missing required initial moveTo") 

302 for pt in pts[:-1]: 

303 self.contour.append((pt, None)) 

304 self.contour.append((pts[-1], "curve")) 

305 

306 def qCurveTo(self, *pts): 

307 if not pts: 

308 raise TypeError("Must pass in at least one point") 

309 if pts[-1] is None: 

310 self.contour = [] 

311 else: 

312 if self.contour is None: 

313 raise PenError("Contour missing required initial moveTo") 

314 for pt in pts[:-1]: 

315 self.contour.append((pt, None)) 

316 if pts[-1] is not None: 

317 self.contour.append((pts[-1], "qcurve")) 

318 

319 def closePath(self): 

320 if self.contour is None: 

321 raise PenError("Contour missing required initial moveTo") 

322 if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]: 

323 self.contour[0] = self.contour[-1] 

324 del self.contour[-1] 

325 else: 

326 # There's an implied line at the end, replace "move" with "line" 

327 # for the first point 

328 pt, tp = self.contour[0] 

329 if tp == "move": 

330 self.contour[0] = pt, "line" 

331 self._flushContour() 

332 self.contour = None 

333 

334 def endPath(self): 

335 if self.contour is None: 

336 raise PenError("Contour missing required initial moveTo") 

337 self._flushContour() 

338 self.contour = None 

339 

340 def addComponent(self, glyphName, transform): 

341 if self.contour is not None: 

342 raise PenError("Components must be added before or after contours") 

343 self.pen.addComponent(glyphName, transform) 

344 

345 

346class GuessSmoothPointPen(AbstractPointPen): 

347 """ 

348 Filtering PointPen that tries to determine whether an on-curve point 

349 should be "smooth", ie. that it's a "tangent" point or a "curve" point. 

350 """ 

351 

352 def __init__(self, outPen, error=0.05): 

353 self._outPen = outPen 

354 self._error = error 

355 self._points = None 

356 

357 def _flushContour(self): 

358 if self._points is None: 

359 raise PenError("Path not begun") 

360 points = self._points 

361 nPoints = len(points) 

362 if not nPoints: 

363 return 

364 if points[0][1] == "move": 

365 # Open path. 

366 indices = range(1, nPoints - 1) 

367 elif nPoints > 1: 

368 # Closed path. To avoid having to mod the contour index, we 

369 # simply abuse Python's negative index feature, and start at -1 

370 indices = range(-1, nPoints - 1) 

371 else: 

372 # closed path containing 1 point (!), ignore. 

373 indices = [] 

374 for i in indices: 

375 pt, segmentType, _, name, kwargs = points[i] 

376 if segmentType is None: 

377 continue 

378 prev = i - 1 

379 next = i + 1 

380 if points[prev][1] is not None and points[next][1] is not None: 

381 continue 

382 # At least one of our neighbors is an off-curve point 

383 pt = points[i][0] 

384 prevPt = points[prev][0] 

385 nextPt = points[next][0] 

386 if pt != prevPt and pt != nextPt: 

387 dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1] 

388 dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1] 

389 a1 = math.atan2(dy1, dx1) 

390 a2 = math.atan2(dy2, dx2) 

391 if abs(a1 - a2) < self._error: 

392 points[i] = pt, segmentType, True, name, kwargs 

393 

394 for pt, segmentType, smooth, name, kwargs in points: 

395 self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) 

396 

397 def beginPath(self, identifier=None, **kwargs): 

398 if self._points is not None: 

399 raise PenError("Path already begun") 

400 self._points = [] 

401 if identifier is not None: 

402 kwargs["identifier"] = identifier 

403 self._outPen.beginPath(**kwargs) 

404 

405 def endPath(self): 

406 self._flushContour() 

407 self._outPen.endPath() 

408 self._points = None 

409 

410 def addPoint( 

411 self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs 

412 ): 

413 if self._points is None: 

414 raise PenError("Path not begun") 

415 if identifier is not None: 

416 kwargs["identifier"] = identifier 

417 self._points.append((pt, segmentType, False, name, kwargs)) 

418 

419 def addComponent(self, glyphName, transformation, identifier=None, **kwargs): 

420 if self._points is not None: 

421 raise PenError("Components must be added before or after contours") 

422 if identifier is not None: 

423 kwargs["identifier"] = identifier 

424 self._outPen.addComponent(glyphName, transformation, **kwargs) 

425 

426 def addVarComponent( 

427 self, glyphName, transformation, location, identifier=None, **kwargs 

428 ): 

429 if self._points is not None: 

430 raise PenError("VarComponents must be added before or after contours") 

431 if identifier is not None: 

432 kwargs["identifier"] = identifier 

433 self._outPen.addVarComponent(glyphName, transformation, location, **kwargs) 

434 

435 

436class ReverseContourPointPen(AbstractPointPen): 

437 """ 

438 This is a PointPen that passes outline data to another PointPen, but 

439 reversing the winding direction of all contours. Components are simply 

440 passed through unchanged. 

441 

442 Closed contours are reversed in such a way that the first point remains 

443 the first point. 

444 """ 

445 

446 def __init__(self, outputPointPen): 

447 self.pen = outputPointPen 

448 # a place to store the points for the current sub path 

449 self.currentContour = None 

450 

451 def _flushContour(self): 

452 pen = self.pen 

453 contour = self.currentContour 

454 if not contour: 

455 pen.beginPath(identifier=self.currentContourIdentifier) 

456 pen.endPath() 

457 return 

458 

459 closed = contour[0][1] != "move" 

460 if not closed: 

461 lastSegmentType = "move" 

462 else: 

463 # Remove the first point and insert it at the end. When 

464 # the list of points gets reversed, this point will then 

465 # again be at the start. In other words, the following 

466 # will hold: 

467 # for N in range(len(originalContour)): 

468 # originalContour[N] == reversedContour[-N] 

469 contour.append(contour.pop(0)) 

470 # Find the first on-curve point. 

471 firstOnCurve = None 

472 for i in range(len(contour)): 

473 if contour[i][1] is not None: 

474 firstOnCurve = i 

475 break 

476 if firstOnCurve is None: 

477 # There are no on-curve points, be basically have to 

478 # do nothing but contour.reverse(). 

479 lastSegmentType = None 

480 else: 

481 lastSegmentType = contour[firstOnCurve][1] 

482 

483 contour.reverse() 

484 if not closed: 

485 # Open paths must start with a move, so we simply dump 

486 # all off-curve points leading up to the first on-curve. 

487 while contour[0][1] is None: 

488 contour.pop(0) 

489 pen.beginPath(identifier=self.currentContourIdentifier) 

490 for pt, nextSegmentType, smooth, name, kwargs in contour: 

491 if nextSegmentType is not None: 

492 segmentType = lastSegmentType 

493 lastSegmentType = nextSegmentType 

494 else: 

495 segmentType = None 

496 pen.addPoint( 

497 pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs 

498 ) 

499 pen.endPath() 

500 

501 def beginPath(self, identifier=None, **kwargs): 

502 if self.currentContour is not None: 

503 raise PenError("Path already begun") 

504 self.currentContour = [] 

505 self.currentContourIdentifier = identifier 

506 self.onCurve = [] 

507 

508 def endPath(self): 

509 if self.currentContour is None: 

510 raise PenError("Path not begun") 

511 self._flushContour() 

512 self.currentContour = None 

513 

514 def addPoint( 

515 self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs 

516 ): 

517 if self.currentContour is None: 

518 raise PenError("Path not begun") 

519 if identifier is not None: 

520 kwargs["identifier"] = identifier 

521 self.currentContour.append((pt, segmentType, smooth, name, kwargs)) 

522 

523 def addComponent(self, glyphName, transform, identifier=None, **kwargs): 

524 if self.currentContour is not None: 

525 raise PenError("Components must be added before or after contours") 

526 self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs) 

527 

528 

529class DecomposingPointPen(LogMixin, AbstractPointPen): 

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

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

532 It can also be used as a mixin class (e.g. see DecomposingRecordingPointPen). 

533 

534 You must override beginPath, addPoint, endPath. You may 

535 additionally override addVarComponent and addComponent. 

536 

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

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

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

540 exception by default. 

541 """ 

542 

543 skipMissingComponents = True 

544 # alias error for convenience 

545 MissingComponentError = MissingComponentError 

546 

547 def __init__( 

548 self, 

549 glyphSet, 

550 *args, 

551 skipMissingComponents=None, 

552 reverseFlipped=False, 

553 **kwargs, 

554 ): 

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

556 as components are looked up by their name. 

557 

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

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

560 to compensate for the flip. 

561 

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

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

564 """ 

565 super().__init__(*args, **kwargs) 

566 self.glyphSet = glyphSet 

567 self.skipMissingComponents = ( 

568 self.__class__.skipMissingComponents 

569 if skipMissingComponents is None 

570 else skipMissingComponents 

571 ) 

572 self.reverseFlipped = reverseFlipped 

573 

574 def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs): 

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

576 

577 The `identifier` parameter and any extra kwargs are ignored. 

578 """ 

579 from fontTools.pens.transformPen import TransformPointPen 

580 

581 try: 

582 glyph = self.glyphSet[baseGlyphName] 

583 except KeyError: 

584 if not self.skipMissingComponents: 

585 raise MissingComponentError(baseGlyphName) 

586 self.log.warning( 

587 "glyph '%s' is missing from glyphSet; skipped" % baseGlyphName 

588 ) 

589 else: 

590 pen = self 

591 if transformation != Identity: 

592 pen = TransformPointPen(pen, transformation) 

593 if self.reverseFlipped: 

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

595 # reverse the contour direction of the component 

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

597 det = a * d - b * c 

598 if a * d - b * c < 0: 

599 pen = ReverseContourPointPen(pen) 

600 glyph.drawPoints(pen)