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

292 statements  

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

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.pens.basePen import AbstractPen, PenError 

19from fontTools.misc.transform import DecomposedTransform 

20 

21__all__ = [ 

22 "AbstractPointPen", 

23 "BasePointToSegmentPen", 

24 "PointToSegmentPen", 

25 "SegmentToPointPen", 

26 "GuessSmoothPointPen", 

27 "ReverseContourPointPen", 

28] 

29 

30 

31class AbstractPointPen: 

32 """Baseclass for all PointPens.""" 

33 

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

35 """Start a new sub path.""" 

36 raise NotImplementedError 

37 

38 def endPath(self) -> None: 

39 """End the current sub path.""" 

40 raise NotImplementedError 

41 

42 def addPoint( 

43 self, 

44 pt: Tuple[float, float], 

45 segmentType: Optional[str] = None, 

46 smooth: bool = False, 

47 name: Optional[str] = None, 

48 identifier: Optional[str] = None, 

49 **kwargs: Any, 

50 ) -> None: 

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

52 raise NotImplementedError 

53 

54 def addComponent( 

55 self, 

56 baseGlyphName: str, 

57 transformation: Tuple[float, float, float, float, float, float], 

58 identifier: Optional[str] = None, 

59 **kwargs: Any, 

60 ) -> None: 

61 """Add a sub glyph.""" 

62 raise NotImplementedError 

63 

64 def addVarComponent( 

65 self, 

66 glyphName: str, 

67 transformation: DecomposedTransform, 

68 location: Dict[str, float], 

69 identifier: Optional[str] = None, 

70 **kwargs: Any, 

71 ) -> None: 

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

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

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

75 to their locations. 

76 """ 

77 # ttGlyphSet decomposes for us 

78 raise AttributeError 

79 

80 

81class BasePointToSegmentPen(AbstractPointPen): 

82 """ 

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

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

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

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

87 care of all the edge cases. 

88 """ 

89 

90 def __init__(self): 

91 self.currentPath = None 

92 

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

94 if self.currentPath is not None: 

95 raise PenError("Path already begun.") 

96 self.currentPath = [] 

97 

98 def _flushContour(self, segments): 

99 """Override this method. 

100 

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

102 of segments: the 'segments' argument. 

103 

104 The segments list contains tuples of length 2: 

105 (segmentType, points) 

106 

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

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

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

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

111 

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

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

114 four items: 

115 (point, smooth, name, kwargs) 

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

117 

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

119 the last point of the last segment. 

120 

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

122 exactly one point tuple. 

123 """ 

124 raise NotImplementedError 

125 

126 def endPath(self): 

127 if self.currentPath is None: 

128 raise PenError("Path not begun.") 

129 points = self.currentPath 

130 self.currentPath = None 

131 if not points: 

132 return 

133 if len(points) == 1: 

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

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

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

137 self._flushContour(segments) 

138 return 

139 segments = [] 

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

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

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

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

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

145 points.pop(0) 

146 else: 

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

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

149 # point. 

150 firstOnCurve = None 

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

152 segmentType = points[i][1] 

153 if segmentType is not None: 

154 firstOnCurve = i 

155 break 

156 if firstOnCurve is None: 

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

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

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

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

161 else: 

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

163 

164 currentSegment = [] 

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

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

167 if segmentType is None: 

168 continue 

169 segments.append((segmentType, currentSegment)) 

170 currentSegment = [] 

171 

172 self._flushContour(segments) 

173 

174 def addPoint( 

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

176 ): 

177 if self.currentPath is None: 

178 raise PenError("Path not begun") 

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

180 

181 

182class PointToSegmentPen(BasePointToSegmentPen): 

183 """ 

184 Adapter class that converts the PointPen protocol to the 

185 (Segment)Pen protocol. 

186 

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

188 and kwargs. 

189 """ 

190 

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

192 BasePointToSegmentPen.__init__(self) 

193 self.pen = segmentPen 

194 self.outputImpliedClosingLine = outputImpliedClosingLine 

195 

196 def _flushContour(self, segments): 

197 if not segments: 

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

199 pen = self.pen 

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

201 # It's an open path. 

202 closed = False 

203 points = segments[0][1] 

204 if len(points) != 1: 

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

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

207 del segments[0] 

208 else: 

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

210 # point of the last segment. 

211 closed = True 

212 segmentType, points = segments[-1] 

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

214 if movePt is None: 

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

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

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

218 pass 

219 else: 

220 pen.moveTo(movePt) 

221 outputImpliedClosingLine = self.outputImpliedClosingLine 

222 nSegments = len(segments) 

223 lastPt = movePt 

224 for i in range(nSegments): 

225 segmentType, points = segments[i] 

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

227 if segmentType == "line": 

228 if len(points) != 1: 

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

230 pt = points[0] 

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

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

233 # starting point don't overlap. 

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

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

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

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

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

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

240 if ( 

241 i + 1 != nSegments 

242 or outputImpliedClosingLine 

243 or not closed 

244 or pt == lastPt 

245 ): 

246 pen.lineTo(pt) 

247 lastPt = pt 

248 elif segmentType == "curve": 

249 pen.curveTo(*points) 

250 lastPt = points[-1] 

251 elif segmentType == "qcurve": 

252 pen.qCurveTo(*points) 

253 lastPt = points[-1] 

254 else: 

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

256 if closed: 

257 pen.closePath() 

258 else: 

259 pen.endPath() 

260 

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

262 del identifier # unused 

263 del kwargs # unused 

264 self.pen.addComponent(glyphName, transform) 

265 

266 

267class SegmentToPointPen(AbstractPen): 

268 """ 

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

270 PointPen protocol. 

271 """ 

272 

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

274 if guessSmooth: 

275 self.pen = GuessSmoothPointPen(pointPen) 

276 else: 

277 self.pen = pointPen 

278 self.contour = None 

279 

280 def _flushContour(self): 

281 pen = self.pen 

282 pen.beginPath() 

283 for pt, segmentType in self.contour: 

284 pen.addPoint(pt, segmentType=segmentType) 

285 pen.endPath() 

286 

287 def moveTo(self, pt): 

288 self.contour = [] 

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

290 

291 def lineTo(self, pt): 

292 if self.contour is None: 

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

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

295 

296 def curveTo(self, *pts): 

297 if not pts: 

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

299 if self.contour is None: 

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

301 for pt in pts[:-1]: 

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

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

304 

305 def qCurveTo(self, *pts): 

306 if not pts: 

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

308 if pts[-1] is None: 

309 self.contour = [] 

310 else: 

311 if self.contour is None: 

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

313 for pt in pts[:-1]: 

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

315 if pts[-1] is not None: 

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

317 

318 def closePath(self): 

319 if self.contour is None: 

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

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

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

323 del self.contour[-1] 

324 else: 

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

326 # for the first point 

327 pt, tp = self.contour[0] 

328 if tp == "move": 

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

330 self._flushContour() 

331 self.contour = None 

332 

333 def endPath(self): 

334 if self.contour is None: 

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

336 self._flushContour() 

337 self.contour = None 

338 

339 def addComponent(self, glyphName, transform): 

340 if self.contour is not None: 

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

342 self.pen.addComponent(glyphName, transform) 

343 

344 

345class GuessSmoothPointPen(AbstractPointPen): 

346 """ 

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

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

349 """ 

350 

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

352 self._outPen = outPen 

353 self._error = error 

354 self._points = None 

355 

356 def _flushContour(self): 

357 if self._points is None: 

358 raise PenError("Path not begun") 

359 points = self._points 

360 nPoints = len(points) 

361 if not nPoints: 

362 return 

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

364 # Open path. 

365 indices = range(1, nPoints - 1) 

366 elif nPoints > 1: 

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

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

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

370 else: 

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

372 indices = [] 

373 for i in indices: 

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

375 if segmentType is None: 

376 continue 

377 prev = i - 1 

378 next = i + 1 

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

380 continue 

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

382 pt = points[i][0] 

383 prevPt = points[prev][0] 

384 nextPt = points[next][0] 

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

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

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

388 a1 = math.atan2(dy1, dx1) 

389 a2 = math.atan2(dy2, dx2) 

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

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

392 

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

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

395 

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

397 if self._points is not None: 

398 raise PenError("Path already begun") 

399 self._points = [] 

400 if identifier is not None: 

401 kwargs["identifier"] = identifier 

402 self._outPen.beginPath(**kwargs) 

403 

404 def endPath(self): 

405 self._flushContour() 

406 self._outPen.endPath() 

407 self._points = None 

408 

409 def addPoint( 

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

411 ): 

412 if self._points is None: 

413 raise PenError("Path not begun") 

414 if identifier is not None: 

415 kwargs["identifier"] = identifier 

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

417 

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

419 if self._points is not None: 

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

421 if identifier is not None: 

422 kwargs["identifier"] = identifier 

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

424 

425 def addVarComponent( 

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

427 ): 

428 if self._points is not None: 

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

430 if identifier is not None: 

431 kwargs["identifier"] = identifier 

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

433 

434 

435class ReverseContourPointPen(AbstractPointPen): 

436 """ 

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

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

439 passed through unchanged. 

440 

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

442 the first point. 

443 """ 

444 

445 def __init__(self, outputPointPen): 

446 self.pen = outputPointPen 

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

448 self.currentContour = None 

449 

450 def _flushContour(self): 

451 pen = self.pen 

452 contour = self.currentContour 

453 if not contour: 

454 pen.beginPath(identifier=self.currentContourIdentifier) 

455 pen.endPath() 

456 return 

457 

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

459 if not closed: 

460 lastSegmentType = "move" 

461 else: 

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

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

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

465 # will hold: 

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

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

468 contour.append(contour.pop(0)) 

469 # Find the first on-curve point. 

470 firstOnCurve = None 

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

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

473 firstOnCurve = i 

474 break 

475 if firstOnCurve is None: 

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

477 # do nothing but contour.reverse(). 

478 lastSegmentType = None 

479 else: 

480 lastSegmentType = contour[firstOnCurve][1] 

481 

482 contour.reverse() 

483 if not closed: 

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

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

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

487 contour.pop(0) 

488 pen.beginPath(identifier=self.currentContourIdentifier) 

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

490 if nextSegmentType is not None: 

491 segmentType = lastSegmentType 

492 lastSegmentType = nextSegmentType 

493 else: 

494 segmentType = None 

495 pen.addPoint( 

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

497 ) 

498 pen.endPath() 

499 

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

501 if self.currentContour is not None: 

502 raise PenError("Path already begun") 

503 self.currentContour = [] 

504 self.currentContourIdentifier = identifier 

505 self.onCurve = [] 

506 

507 def endPath(self): 

508 if self.currentContour is None: 

509 raise PenError("Path not begun") 

510 self._flushContour() 

511 self.currentContour = None 

512 

513 def addPoint( 

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

515 ): 

516 if self.currentContour is None: 

517 raise PenError("Path not begun") 

518 if identifier is not None: 

519 kwargs["identifier"] = identifier 

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

521 

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

523 if self.currentContour is not None: 

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

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