Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/fontTools/ttLib/tables/_g_l_y_f.py: 13%

1566 statements  

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

1"""_g_l_y_f.py -- Converter classes for the 'glyf' table.""" 

2 

3from collections import namedtuple 

4from fontTools.misc import sstruct 

5from fontTools import ttLib 

6from fontTools import version 

7from fontTools.misc.transform import DecomposedTransform 

8from fontTools.misc.textTools import tostr, safeEval, pad 

9from fontTools.misc.arrayTools import calcIntBounds, pointInRect 

10from fontTools.misc.bezierTools import calcQuadraticBounds 

11from fontTools.misc.fixedTools import ( 

12 fixedToFloat as fi2fl, 

13 floatToFixed as fl2fi, 

14 floatToFixedToStr as fl2str, 

15 strToFixedToFloat as str2fl, 

16 otRound, 

17) 

18from numbers import Number 

19from . import DefaultTable 

20from . import ttProgram 

21import sys 

22import struct 

23import array 

24import logging 

25import math 

26import os 

27from fontTools.misc import xmlWriter 

28from fontTools.misc.filenames import userNameToFileName 

29from fontTools.misc.loggingTools import deprecateFunction 

30from enum import IntFlag 

31from types import SimpleNamespace 

32from typing import Set 

33 

34log = logging.getLogger(__name__) 

35 

36# We compute the version the same as is computed in ttlib/__init__ 

37# so that we can write 'ttLibVersion' attribute of the glyf TTX files 

38# when glyf is written to separate files. 

39version = ".".join(version.split(".")[:2]) 

40 

41# 

42# The Apple and MS rasterizers behave differently for 

43# scaled composite components: one does scale first and then translate 

44# and the other does it vice versa. MS defined some flags to indicate 

45# the difference, but it seems nobody actually _sets_ those flags. 

46# 

47# Funny thing: Apple seems to _only_ do their thing in the 

48# WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE 

49# (eg. Charcoal)... 

50# 

51SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple 

52 

53 

54class table__g_l_y_f(DefaultTable.DefaultTable): 

55 """Glyph Data Table 

56 

57 This class represents the `glyf <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf>`_ 

58 table, which contains outlines for glyphs in TrueType format. In many cases, 

59 it is easier to access and manipulate glyph outlines through the ``GlyphSet`` 

60 object returned from :py:meth:`fontTools.ttLib.ttFont.getGlyphSet`:: 

61 

62 >> from fontTools.pens.boundsPen import BoundsPen 

63 >> glyphset = font.getGlyphSet() 

64 >> bp = BoundsPen(glyphset) 

65 >> glyphset["A"].draw(bp) 

66 >> bp.bounds 

67 (19, 0, 633, 716) 

68 

69 However, this class can be used for low-level access to the ``glyf`` table data. 

70 Objects of this class support dictionary-like access, mapping glyph names to 

71 :py:class:`Glyph` objects:: 

72 

73 >> glyf = font["glyf"] 

74 >> len(glyf["Aacute"].components) 

75 2 

76 

77 Note that when adding glyphs to the font via low-level access to the ``glyf`` 

78 table, the new glyphs must also be added to the ``hmtx``/``vmtx`` table:: 

79 

80 >> font["glyf"]["divisionslash"] = Glyph() 

81 >> font["hmtx"]["divisionslash"] = (640, 0) 

82 

83 """ 

84 

85 dependencies = ["fvar"] 

86 

87 # this attribute controls the amount of padding applied to glyph data upon compile. 

88 # Glyph lenghts are aligned to multiples of the specified value. 

89 # Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means 

90 # no padding, except for when padding would allow to use short loca offsets. 

91 padding = 1 

92 

93 def decompile(self, data, ttFont): 

94 self.axisTags = ( 

95 [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else [] 

96 ) 

97 loca = ttFont["loca"] 

98 pos = int(loca[0]) 

99 nextPos = 0 

100 noname = 0 

101 self.glyphs = {} 

102 self.glyphOrder = glyphOrder = ttFont.getGlyphOrder() 

103 for i in range(0, len(loca) - 1): 

104 try: 

105 glyphName = glyphOrder[i] 

106 except IndexError: 

107 noname = noname + 1 

108 glyphName = "ttxautoglyph%s" % i 

109 nextPos = int(loca[i + 1]) 

110 glyphdata = data[pos:nextPos] 

111 if len(glyphdata) != (nextPos - pos): 

112 raise ttLib.TTLibError("not enough 'glyf' table data") 

113 glyph = Glyph(glyphdata) 

114 self.glyphs[glyphName] = glyph 

115 pos = nextPos 

116 if len(data) - nextPos >= 4: 

117 log.warning( 

118 "too much 'glyf' table data: expected %d, received %d bytes", 

119 nextPos, 

120 len(data), 

121 ) 

122 if noname: 

123 log.warning("%s glyphs have no name", noname) 

124 if ttFont.lazy is False: # Be lazy for None and True 

125 self.ensureDecompiled() 

126 

127 def ensureDecompiled(self, recurse=False): 

128 # The recurse argument is unused, but part of the signature of 

129 # ensureDecompiled across the library. 

130 for glyph in self.glyphs.values(): 

131 glyph.expand(self) 

132 

133 def compile(self, ttFont): 

134 self.axisTags = ( 

135 [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else [] 

136 ) 

137 if not hasattr(self, "glyphOrder"): 

138 self.glyphOrder = ttFont.getGlyphOrder() 

139 padding = self.padding 

140 assert padding in (0, 1, 2, 4) 

141 locations = [] 

142 currentLocation = 0 

143 dataList = [] 

144 recalcBBoxes = ttFont.recalcBBoxes 

145 for glyphName in self.glyphOrder: 

146 glyph = self.glyphs[glyphName] 

147 glyphData = glyph.compile(self, recalcBBoxes) 

148 if padding > 1: 

149 glyphData = pad(glyphData, size=padding) 

150 locations.append(currentLocation) 

151 currentLocation = currentLocation + len(glyphData) 

152 dataList.append(glyphData) 

153 locations.append(currentLocation) 

154 

155 if padding == 1 and currentLocation < 0x20000: 

156 # See if we can pad any odd-lengthed glyphs to allow loca 

157 # table to use the short offsets. 

158 indices = [ 

159 i for i, glyphData in enumerate(dataList) if len(glyphData) % 2 == 1 

160 ] 

161 if indices and currentLocation + len(indices) < 0x20000: 

162 # It fits. Do it. 

163 for i in indices: 

164 dataList[i] += b"\0" 

165 currentLocation = 0 

166 for i, glyphData in enumerate(dataList): 

167 locations[i] = currentLocation 

168 currentLocation += len(glyphData) 

169 locations[len(dataList)] = currentLocation 

170 

171 data = b"".join(dataList) 

172 if "loca" in ttFont: 

173 ttFont["loca"].set(locations) 

174 if "maxp" in ttFont: 

175 ttFont["maxp"].numGlyphs = len(self.glyphs) 

176 if not data: 

177 # As a special case when all glyph in the font are empty, add a zero byte 

178 # to the table, so that OTS doesn’t reject it, and to make the table work 

179 # on Windows as well. 

180 # See https://github.com/khaledhosny/ots/issues/52 

181 data = b"\0" 

182 return data 

183 

184 def toXML(self, writer, ttFont, splitGlyphs=False): 

185 notice = ( 

186 "The xMin, yMin, xMax and yMax values\n" 

187 "will be recalculated by the compiler." 

188 ) 

189 glyphNames = ttFont.getGlyphNames() 

190 if not splitGlyphs: 

191 writer.newline() 

192 writer.comment(notice) 

193 writer.newline() 

194 writer.newline() 

195 numGlyphs = len(glyphNames) 

196 if splitGlyphs: 

197 path, ext = os.path.splitext(writer.file.name) 

198 existingGlyphFiles = set() 

199 for glyphName in glyphNames: 

200 glyph = self.get(glyphName) 

201 if glyph is None: 

202 log.warning("glyph '%s' does not exist in glyf table", glyphName) 

203 continue 

204 if glyph.numberOfContours: 

205 if splitGlyphs: 

206 glyphPath = userNameToFileName( 

207 tostr(glyphName, "utf-8"), 

208 existingGlyphFiles, 

209 prefix=path + ".", 

210 suffix=ext, 

211 ) 

212 existingGlyphFiles.add(glyphPath.lower()) 

213 glyphWriter = xmlWriter.XMLWriter( 

214 glyphPath, 

215 idlefunc=writer.idlefunc, 

216 newlinestr=writer.newlinestr, 

217 ) 

218 glyphWriter.begintag("ttFont", ttLibVersion=version) 

219 glyphWriter.newline() 

220 glyphWriter.begintag("glyf") 

221 glyphWriter.newline() 

222 glyphWriter.comment(notice) 

223 glyphWriter.newline() 

224 writer.simpletag("TTGlyph", src=os.path.basename(glyphPath)) 

225 else: 

226 glyphWriter = writer 

227 glyphWriter.begintag( 

228 "TTGlyph", 

229 [ 

230 ("name", glyphName), 

231 ("xMin", glyph.xMin), 

232 ("yMin", glyph.yMin), 

233 ("xMax", glyph.xMax), 

234 ("yMax", glyph.yMax), 

235 ], 

236 ) 

237 glyphWriter.newline() 

238 glyph.toXML(glyphWriter, ttFont) 

239 glyphWriter.endtag("TTGlyph") 

240 glyphWriter.newline() 

241 if splitGlyphs: 

242 glyphWriter.endtag("glyf") 

243 glyphWriter.newline() 

244 glyphWriter.endtag("ttFont") 

245 glyphWriter.newline() 

246 glyphWriter.close() 

247 else: 

248 writer.simpletag("TTGlyph", name=glyphName) 

249 writer.comment("contains no outline data") 

250 if not splitGlyphs: 

251 writer.newline() 

252 writer.newline() 

253 

254 def fromXML(self, name, attrs, content, ttFont): 

255 if name != "TTGlyph": 

256 return 

257 if not hasattr(self, "glyphs"): 

258 self.glyphs = {} 

259 if not hasattr(self, "glyphOrder"): 

260 self.glyphOrder = ttFont.getGlyphOrder() 

261 glyphName = attrs["name"] 

262 log.debug("unpacking glyph '%s'", glyphName) 

263 glyph = Glyph() 

264 for attr in ["xMin", "yMin", "xMax", "yMax"]: 

265 setattr(glyph, attr, safeEval(attrs.get(attr, "0"))) 

266 self.glyphs[glyphName] = glyph 

267 for element in content: 

268 if not isinstance(element, tuple): 

269 continue 

270 name, attrs, content = element 

271 glyph.fromXML(name, attrs, content, ttFont) 

272 if not ttFont.recalcBBoxes: 

273 glyph.compact(self, 0) 

274 

275 def setGlyphOrder(self, glyphOrder): 

276 """Sets the glyph order 

277 

278 Args: 

279 glyphOrder ([str]): List of glyph names in order. 

280 """ 

281 self.glyphOrder = glyphOrder 

282 

283 def getGlyphName(self, glyphID): 

284 """Returns the name for the glyph with the given ID. 

285 

286 Raises a ``KeyError`` if the glyph name is not found in the font. 

287 """ 

288 return self.glyphOrder[glyphID] 

289 

290 def getGlyphID(self, glyphName): 

291 """Returns the ID of the glyph with the given name. 

292 

293 Raises a ``ValueError`` if the glyph is not found in the font. 

294 """ 

295 # XXX optimize with reverse dict!!! 

296 return self.glyphOrder.index(glyphName) 

297 

298 def removeHinting(self): 

299 """Removes TrueType hints from all glyphs in the glyphset. 

300 

301 See :py:meth:`Glyph.removeHinting`. 

302 """ 

303 for glyph in self.glyphs.values(): 

304 glyph.removeHinting() 

305 

306 def keys(self): 

307 return self.glyphs.keys() 

308 

309 def has_key(self, glyphName): 

310 return glyphName in self.glyphs 

311 

312 __contains__ = has_key 

313 

314 def get(self, glyphName, default=None): 

315 glyph = self.glyphs.get(glyphName, default) 

316 if glyph is not None: 

317 glyph.expand(self) 

318 return glyph 

319 

320 def __getitem__(self, glyphName): 

321 glyph = self.glyphs[glyphName] 

322 glyph.expand(self) 

323 return glyph 

324 

325 def __setitem__(self, glyphName, glyph): 

326 self.glyphs[glyphName] = glyph 

327 if glyphName not in self.glyphOrder: 

328 self.glyphOrder.append(glyphName) 

329 

330 def __delitem__(self, glyphName): 

331 del self.glyphs[glyphName] 

332 self.glyphOrder.remove(glyphName) 

333 

334 def __len__(self): 

335 assert len(self.glyphOrder) == len(self.glyphs) 

336 return len(self.glyphs) 

337 

338 def _getPhantomPoints(self, glyphName, hMetrics, vMetrics=None): 

339 """Compute the four "phantom points" for the given glyph from its bounding box 

340 and the horizontal and vertical advance widths and sidebearings stored in the 

341 ttFont's "hmtx" and "vmtx" tables. 

342 

343 'hMetrics' should be ttFont['hmtx'].metrics. 

344 

345 'vMetrics' should be ttFont['vmtx'].metrics if there is "vmtx" or None otherwise. 

346 If there is no vMetrics passed in, vertical phantom points are set to the zero coordinate. 

347 

348 https://docs.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms 

349 """ 

350 glyph = self[glyphName] 

351 if not hasattr(glyph, "xMin"): 

352 glyph.recalcBounds(self) 

353 

354 horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName] 

355 leftSideX = glyph.xMin - leftSideBearing 

356 rightSideX = leftSideX + horizontalAdvanceWidth 

357 

358 if vMetrics: 

359 verticalAdvanceWidth, topSideBearing = vMetrics[glyphName] 

360 topSideY = topSideBearing + glyph.yMax 

361 bottomSideY = topSideY - verticalAdvanceWidth 

362 else: 

363 bottomSideY = topSideY = 0 

364 

365 return [ 

366 (leftSideX, 0), 

367 (rightSideX, 0), 

368 (0, topSideY), 

369 (0, bottomSideY), 

370 ] 

371 

372 def _getCoordinatesAndControls(self, glyphName, hMetrics, vMetrics=None): 

373 """Return glyph coordinates and controls as expected by "gvar" table. 

374 

375 The coordinates includes four "phantom points" for the glyph metrics, 

376 as mandated by the "gvar" spec. 

377 

378 The glyph controls is a namedtuple with the following attributes: 

379 - numberOfContours: -1 for composite glyphs. 

380 - endPts: list of indices of end points for each contour in simple 

381 glyphs, or component indices in composite glyphs (used for IUP 

382 optimization). 

383 - flags: array of contour point flags for simple glyphs (None for 

384 composite glyphs). 

385 - components: list of base glyph names (str) for each component in 

386 composite glyphs (None for simple glyphs). 

387 

388 The "hMetrics" and vMetrics are used to compute the "phantom points" (see 

389 the "_getPhantomPoints" method). 

390 

391 Return None if the requested glyphName is not present. 

392 """ 

393 glyph = self.get(glyphName) 

394 if glyph is None: 

395 return None 

396 if glyph.isComposite(): 

397 coords = GlyphCoordinates( 

398 [(getattr(c, "x", 0), getattr(c, "y", 0)) for c in glyph.components] 

399 ) 

400 controls = _GlyphControls( 

401 numberOfContours=glyph.numberOfContours, 

402 endPts=list(range(len(glyph.components))), 

403 flags=None, 

404 components=[ 

405 (c.glyphName, getattr(c, "transform", None)) 

406 for c in glyph.components 

407 ], 

408 ) 

409 elif glyph.isVarComposite(): 

410 coords = [] 

411 controls = [] 

412 

413 for component in glyph.components: 

414 

415 ( 

416 componentCoords, 

417 componentControls, 

418 ) = component.getCoordinatesAndControls() 

419 coords.extend(componentCoords) 

420 controls.extend(componentControls) 

421 

422 coords = GlyphCoordinates(coords) 

423 

424 controls = _GlyphControls( 

425 numberOfContours=glyph.numberOfContours, 

426 endPts=list(range(len(coords))), 

427 flags=None, 

428 components=[ 

429 (c.glyphName, getattr(c, "flags", None)) for c in glyph.components 

430 ], 

431 ) 

432 

433 else: 

434 coords, endPts, flags = glyph.getCoordinates(self) 

435 coords = coords.copy() 

436 controls = _GlyphControls( 

437 numberOfContours=glyph.numberOfContours, 

438 endPts=endPts, 

439 flags=flags, 

440 components=None, 

441 ) 

442 # Add phantom points for (left, right, top, bottom) positions. 

443 phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics) 

444 coords.extend(phantomPoints) 

445 return coords, controls 

446 

447 def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None): 

448 """Set coordinates and metrics for the given glyph. 

449 

450 "coord" is an array of GlyphCoordinates which must include the "phantom 

451 points" as the last four coordinates. 

452 

453 Both the horizontal/vertical advances and left/top sidebearings in "hmtx" 

454 and "vmtx" tables (if any) are updated from four phantom points and 

455 the glyph's bounding boxes. 

456 

457 The "hMetrics" and vMetrics are used to propagate "phantom points" 

458 into "hmtx" and "vmtx" tables if desired. (see the "_getPhantomPoints" 

459 method). 

460 """ 

461 glyph = self[glyphName] 

462 

463 # Handle phantom points for (left, right, top, bottom) positions. 

464 assert len(coord) >= 4 

465 leftSideX = coord[-4][0] 

466 rightSideX = coord[-3][0] 

467 topSideY = coord[-2][1] 

468 bottomSideY = coord[-1][1] 

469 

470 coord = coord[:-4] 

471 

472 if glyph.isComposite(): 

473 assert len(coord) == len(glyph.components) 

474 for p, comp in zip(coord, glyph.components): 

475 if hasattr(comp, "x"): 

476 comp.x, comp.y = p 

477 elif glyph.isVarComposite(): 

478 for comp in glyph.components: 

479 coord = comp.setCoordinates(coord) 

480 assert not coord 

481 elif glyph.numberOfContours == 0: 

482 assert len(coord) == 0 

483 else: 

484 assert len(coord) == len(glyph.coordinates) 

485 glyph.coordinates = GlyphCoordinates(coord) 

486 

487 glyph.recalcBounds(self) 

488 

489 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 

490 if horizontalAdvanceWidth < 0: 

491 # unlikely, but it can happen, see: 

492 # https://github.com/fonttools/fonttools/pull/1198 

493 horizontalAdvanceWidth = 0 

494 leftSideBearing = otRound(glyph.xMin - leftSideX) 

495 hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing 

496 

497 if vMetrics is not None: 

498 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 

499 if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal 

500 verticalAdvanceWidth = 0 

501 topSideBearing = otRound(topSideY - glyph.yMax) 

502 vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing 

503 

504 # Deprecated 

505 

506 def _synthesizeVMetrics(self, glyphName, ttFont, defaultVerticalOrigin): 

507 """This method is wrong and deprecated. 

508 For rationale see: 

509 https://github.com/fonttools/fonttools/pull/2266/files#r613569473 

510 """ 

511 vMetrics = getattr(ttFont.get("vmtx"), "metrics", None) 

512 if vMetrics is None: 

513 verticalAdvanceWidth = ttFont["head"].unitsPerEm 

514 topSideY = getattr(ttFont.get("hhea"), "ascent", None) 

515 if topSideY is None: 

516 if defaultVerticalOrigin is not None: 

517 topSideY = defaultVerticalOrigin 

518 else: 

519 topSideY = verticalAdvanceWidth 

520 glyph = self[glyphName] 

521 glyph.recalcBounds(self) 

522 topSideBearing = otRound(topSideY - glyph.yMax) 

523 vMetrics = {glyphName: (verticalAdvanceWidth, topSideBearing)} 

524 return vMetrics 

525 

526 @deprecateFunction("use '_getPhantomPoints' instead", category=DeprecationWarning) 

527 def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None): 

528 """Old public name for self._getPhantomPoints(). 

529 See: https://github.com/fonttools/fonttools/pull/2266""" 

530 hMetrics = ttFont["hmtx"].metrics 

531 vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin) 

532 return self._getPhantomPoints(glyphName, hMetrics, vMetrics) 

533 

534 @deprecateFunction( 

535 "use '_getCoordinatesAndControls' instead", category=DeprecationWarning 

536 ) 

537 def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None): 

538 """Old public name for self._getCoordinatesAndControls(). 

539 See: https://github.com/fonttools/fonttools/pull/2266""" 

540 hMetrics = ttFont["hmtx"].metrics 

541 vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin) 

542 return self._getCoordinatesAndControls(glyphName, hMetrics, vMetrics) 

543 

544 @deprecateFunction("use '_setCoordinates' instead", category=DeprecationWarning) 

545 def setCoordinates(self, glyphName, ttFont): 

546 """Old public name for self._setCoordinates(). 

547 See: https://github.com/fonttools/fonttools/pull/2266""" 

548 hMetrics = ttFont["hmtx"].metrics 

549 vMetrics = getattr(ttFont.get("vmtx"), "metrics", None) 

550 self._setCoordinates(glyphName, hMetrics, vMetrics) 

551 

552 

553_GlyphControls = namedtuple( 

554 "_GlyphControls", "numberOfContours endPts flags components" 

555) 

556 

557 

558glyphHeaderFormat = """ 

559 > # big endian 

560 numberOfContours: h 

561 xMin: h 

562 yMin: h 

563 xMax: h 

564 yMax: h 

565""" 

566 

567# flags 

568flagOnCurve = 0x01 

569flagXShort = 0x02 

570flagYShort = 0x04 

571flagRepeat = 0x08 

572flagXsame = 0x10 

573flagYsame = 0x20 

574flagOverlapSimple = 0x40 

575flagCubic = 0x80 

576 

577# These flags are kept for XML output after decompiling the coordinates 

578keepFlags = flagOnCurve + flagOverlapSimple + flagCubic 

579 

580_flagSignBytes = { 

581 0: 2, 

582 flagXsame: 0, 

583 flagXShort | flagXsame: +1, 

584 flagXShort: -1, 

585 flagYsame: 0, 

586 flagYShort | flagYsame: +1, 

587 flagYShort: -1, 

588} 

589 

590 

591def flagBest(x, y, onCurve): 

592 """For a given x,y delta pair, returns the flag that packs this pair 

593 most efficiently, as well as the number of byte cost of such flag.""" 

594 

595 flag = flagOnCurve if onCurve else 0 

596 cost = 0 

597 # do x 

598 if x == 0: 

599 flag = flag | flagXsame 

600 elif -255 <= x <= 255: 

601 flag = flag | flagXShort 

602 if x > 0: 

603 flag = flag | flagXsame 

604 cost += 1 

605 else: 

606 cost += 2 

607 # do y 

608 if y == 0: 

609 flag = flag | flagYsame 

610 elif -255 <= y <= 255: 

611 flag = flag | flagYShort 

612 if y > 0: 

613 flag = flag | flagYsame 

614 cost += 1 

615 else: 

616 cost += 2 

617 return flag, cost 

618 

619 

620def flagFits(newFlag, oldFlag, mask): 

621 newBytes = _flagSignBytes[newFlag & mask] 

622 oldBytes = _flagSignBytes[oldFlag & mask] 

623 return newBytes == oldBytes or abs(newBytes) > abs(oldBytes) 

624 

625 

626def flagSupports(newFlag, oldFlag): 

627 return ( 

628 (oldFlag & flagOnCurve) == (newFlag & flagOnCurve) 

629 and flagFits(newFlag, oldFlag, flagXsame | flagXShort) 

630 and flagFits(newFlag, oldFlag, flagYsame | flagYShort) 

631 ) 

632 

633 

634def flagEncodeCoord(flag, mask, coord, coordBytes): 

635 byteCount = _flagSignBytes[flag & mask] 

636 if byteCount == 1: 

637 coordBytes.append(coord) 

638 elif byteCount == -1: 

639 coordBytes.append(-coord) 

640 elif byteCount == 2: 

641 coordBytes.extend(struct.pack(">h", coord)) 

642 

643 

644def flagEncodeCoords(flag, x, y, xBytes, yBytes): 

645 flagEncodeCoord(flag, flagXsame | flagXShort, x, xBytes) 

646 flagEncodeCoord(flag, flagYsame | flagYShort, y, yBytes) 

647 

648 

649ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes 

650ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points 

651ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true 

652WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0 

653NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!) 

654MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one 

655WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy 

656WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11 

657WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow 

658USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph 

659OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts 

660SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple) 

661UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS) 

662 

663 

664CompositeMaxpValues = namedtuple( 

665 "CompositeMaxpValues", ["nPoints", "nContours", "maxComponentDepth"] 

666) 

667 

668 

669class Glyph(object): 

670 """This class represents an individual TrueType glyph. 

671 

672 TrueType glyph objects come in two flavours: simple and composite. Simple 

673 glyph objects contain contours, represented via the ``.coordinates``, 

674 ``.flags``, ``.numberOfContours``, and ``.endPtsOfContours`` attributes; 

675 composite glyphs contain components, available through the ``.components`` 

676 attributes. 

677 

678 Because the ``.coordinates`` attribute (and other simple glyph attributes mentioned 

679 above) is only set on simple glyphs and the ``.components`` attribute is only 

680 set on composite glyphs, it is necessary to use the :py:meth:`isComposite` 

681 method to test whether a glyph is simple or composite before attempting to 

682 access its data. 

683 

684 For a composite glyph, the components can also be accessed via array-like access:: 

685 

686 >> assert(font["glyf"]["Aacute"].isComposite()) 

687 >> font["glyf"]["Aacute"][0] 

688 <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0> 

689 

690 """ 

691 

692 def __init__(self, data=b""): 

693 if not data: 

694 # empty char 

695 self.numberOfContours = 0 

696 return 

697 self.data = data 

698 

699 def compact(self, glyfTable, recalcBBoxes=True): 

700 data = self.compile(glyfTable, recalcBBoxes) 

701 self.__dict__.clear() 

702 self.data = data 

703 

704 def expand(self, glyfTable): 

705 if not hasattr(self, "data"): 

706 # already unpacked 

707 return 

708 if not self.data: 

709 # empty char 

710 del self.data 

711 self.numberOfContours = 0 

712 return 

713 dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self) 

714 del self.data 

715 # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in 

716 # some glyphs; decompileCoordinates assumes that there's at least 

717 # one, so short-circuit here. 

718 if self.numberOfContours == 0: 

719 return 

720 if self.isComposite(): 

721 self.decompileComponents(data, glyfTable) 

722 elif self.isVarComposite(): 

723 self.decompileVarComponents(data, glyfTable) 

724 else: 

725 self.decompileCoordinates(data) 

726 

727 def compile(self, glyfTable, recalcBBoxes=True): 

728 if hasattr(self, "data"): 

729 if recalcBBoxes: 

730 # must unpack glyph in order to recalculate bounding box 

731 self.expand(glyfTable) 

732 else: 

733 return self.data 

734 if self.numberOfContours == 0: 

735 return b"" 

736 if recalcBBoxes: 

737 self.recalcBounds(glyfTable) 

738 data = sstruct.pack(glyphHeaderFormat, self) 

739 if self.isComposite(): 

740 data = data + self.compileComponents(glyfTable) 

741 elif self.isVarComposite(): 

742 data = data + self.compileVarComponents(glyfTable) 

743 else: 

744 data = data + self.compileCoordinates() 

745 return data 

746 

747 def toXML(self, writer, ttFont): 

748 if self.isComposite(): 

749 for compo in self.components: 

750 compo.toXML(writer, ttFont) 

751 haveInstructions = hasattr(self, "program") 

752 elif self.isVarComposite(): 

753 for compo in self.components: 

754 compo.toXML(writer, ttFont) 

755 haveInstructions = False 

756 else: 

757 last = 0 

758 for i in range(self.numberOfContours): 

759 writer.begintag("contour") 

760 writer.newline() 

761 for j in range(last, self.endPtsOfContours[i] + 1): 

762 attrs = [ 

763 ("x", self.coordinates[j][0]), 

764 ("y", self.coordinates[j][1]), 

765 ("on", self.flags[j] & flagOnCurve), 

766 ] 

767 if self.flags[j] & flagOverlapSimple: 

768 # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours 

769 attrs.append(("overlap", 1)) 

770 if self.flags[j] & flagCubic: 

771 attrs.append(("cubic", 1)) 

772 writer.simpletag("pt", attrs) 

773 writer.newline() 

774 last = self.endPtsOfContours[i] + 1 

775 writer.endtag("contour") 

776 writer.newline() 

777 haveInstructions = self.numberOfContours > 0 

778 if haveInstructions: 

779 if self.program: 

780 writer.begintag("instructions") 

781 writer.newline() 

782 self.program.toXML(writer, ttFont) 

783 writer.endtag("instructions") 

784 else: 

785 writer.simpletag("instructions") 

786 writer.newline() 

787 

788 def fromXML(self, name, attrs, content, ttFont): 

789 if name == "contour": 

790 if self.numberOfContours < 0: 

791 raise ttLib.TTLibError("can't mix composites and contours in glyph") 

792 self.numberOfContours = self.numberOfContours + 1 

793 coordinates = GlyphCoordinates() 

794 flags = bytearray() 

795 for element in content: 

796 if not isinstance(element, tuple): 

797 continue 

798 name, attrs, content = element 

799 if name != "pt": 

800 continue # ignore anything but "pt" 

801 coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"]))) 

802 flag = bool(safeEval(attrs["on"])) 

803 if "overlap" in attrs and bool(safeEval(attrs["overlap"])): 

804 flag |= flagOverlapSimple 

805 if "cubic" in attrs and bool(safeEval(attrs["cubic"])): 

806 flag |= flagCubic 

807 flags.append(flag) 

808 if not hasattr(self, "coordinates"): 

809 self.coordinates = coordinates 

810 self.flags = flags 

811 self.endPtsOfContours = [len(coordinates) - 1] 

812 else: 

813 self.coordinates.extend(coordinates) 

814 self.flags.extend(flags) 

815 self.endPtsOfContours.append(len(self.coordinates) - 1) 

816 elif name == "component": 

817 if self.numberOfContours > 0: 

818 raise ttLib.TTLibError("can't mix composites and contours in glyph") 

819 self.numberOfContours = -1 

820 if not hasattr(self, "components"): 

821 self.components = [] 

822 component = GlyphComponent() 

823 self.components.append(component) 

824 component.fromXML(name, attrs, content, ttFont) 

825 elif name == "varComponent": 

826 if self.numberOfContours > 0: 

827 raise ttLib.TTLibError("can't mix composites and contours in glyph") 

828 self.numberOfContours = -2 

829 if not hasattr(self, "components"): 

830 self.components = [] 

831 component = GlyphVarComponent() 

832 self.components.append(component) 

833 component.fromXML(name, attrs, content, ttFont) 

834 elif name == "instructions": 

835 self.program = ttProgram.Program() 

836 for element in content: 

837 if not isinstance(element, tuple): 

838 continue 

839 name, attrs, content = element 

840 self.program.fromXML(name, attrs, content, ttFont) 

841 

842 def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): 

843 assert self.isComposite() or self.isVarComposite() 

844 nContours = 0 

845 nPoints = 0 

846 initialMaxComponentDepth = maxComponentDepth 

847 for compo in self.components: 

848 baseGlyph = glyfTable[compo.glyphName] 

849 if baseGlyph.numberOfContours == 0: 

850 continue 

851 elif baseGlyph.numberOfContours > 0: 

852 nP, nC = baseGlyph.getMaxpValues() 

853 else: 

854 nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues( 

855 glyfTable, initialMaxComponentDepth + 1 

856 ) 

857 maxComponentDepth = max(maxComponentDepth, componentDepth) 

858 nPoints = nPoints + nP 

859 nContours = nContours + nC 

860 return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) 

861 

862 def getMaxpValues(self): 

863 assert self.numberOfContours > 0 

864 return len(self.coordinates), len(self.endPtsOfContours) 

865 

866 def decompileComponents(self, data, glyfTable): 

867 self.components = [] 

868 more = 1 

869 haveInstructions = 0 

870 while more: 

871 component = GlyphComponent() 

872 more, haveInstr, data = component.decompile(data, glyfTable) 

873 haveInstructions = haveInstructions | haveInstr 

874 self.components.append(component) 

875 if haveInstructions: 

876 (numInstructions,) = struct.unpack(">h", data[:2]) 

877 data = data[2:] 

878 self.program = ttProgram.Program() 

879 self.program.fromBytecode(data[:numInstructions]) 

880 data = data[numInstructions:] 

881 if len(data) >= 4: 

882 log.warning( 

883 "too much glyph data at the end of composite glyph: %d excess bytes", 

884 len(data), 

885 ) 

886 

887 def decompileVarComponents(self, data, glyfTable): 

888 self.components = [] 

889 while len(data) >= GlyphVarComponent.MIN_SIZE: 

890 component = GlyphVarComponent() 

891 data = component.decompile(data, glyfTable) 

892 self.components.append(component) 

893 

894 def decompileCoordinates(self, data): 

895 endPtsOfContours = array.array("H") 

896 endPtsOfContours.frombytes(data[: 2 * self.numberOfContours]) 

897 if sys.byteorder != "big": 

898 endPtsOfContours.byteswap() 

899 self.endPtsOfContours = endPtsOfContours.tolist() 

900 

901 pos = 2 * self.numberOfContours 

902 (instructionLength,) = struct.unpack(">h", data[pos : pos + 2]) 

903 self.program = ttProgram.Program() 

904 self.program.fromBytecode(data[pos + 2 : pos + 2 + instructionLength]) 

905 pos += 2 + instructionLength 

906 nCoordinates = self.endPtsOfContours[-1] + 1 

907 flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw( 

908 nCoordinates, data, pos 

909 ) 

910 

911 # fill in repetitions and apply signs 

912 self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates) 

913 xIndex = 0 

914 yIndex = 0 

915 for i in range(nCoordinates): 

916 flag = flags[i] 

917 # x coordinate 

918 if flag & flagXShort: 

919 if flag & flagXsame: 

920 x = xCoordinates[xIndex] 

921 else: 

922 x = -xCoordinates[xIndex] 

923 xIndex = xIndex + 1 

924 elif flag & flagXsame: 

925 x = 0 

926 else: 

927 x = xCoordinates[xIndex] 

928 xIndex = xIndex + 1 

929 # y coordinate 

930 if flag & flagYShort: 

931 if flag & flagYsame: 

932 y = yCoordinates[yIndex] 

933 else: 

934 y = -yCoordinates[yIndex] 

935 yIndex = yIndex + 1 

936 elif flag & flagYsame: 

937 y = 0 

938 else: 

939 y = yCoordinates[yIndex] 

940 yIndex = yIndex + 1 

941 coordinates[i] = (x, y) 

942 assert xIndex == len(xCoordinates) 

943 assert yIndex == len(yCoordinates) 

944 coordinates.relativeToAbsolute() 

945 # discard all flags except "keepFlags" 

946 for i in range(len(flags)): 

947 flags[i] &= keepFlags 

948 self.flags = flags 

949 

950 def decompileCoordinatesRaw(self, nCoordinates, data, pos=0): 

951 # unpack flags and prepare unpacking of coordinates 

952 flags = bytearray(nCoordinates) 

953 # Warning: deep Python trickery going on. We use the struct module to unpack 

954 # the coordinates. We build a format string based on the flags, so we can 

955 # unpack the coordinates in one struct.unpack() call. 

956 xFormat = ">" # big endian 

957 yFormat = ">" # big endian 

958 j = 0 

959 while True: 

960 flag = data[pos] 

961 pos += 1 

962 repeat = 1 

963 if flag & flagRepeat: 

964 repeat = data[pos] + 1 

965 pos += 1 

966 for k in range(repeat): 

967 if flag & flagXShort: 

968 xFormat = xFormat + "B" 

969 elif not (flag & flagXsame): 

970 xFormat = xFormat + "h" 

971 if flag & flagYShort: 

972 yFormat = yFormat + "B" 

973 elif not (flag & flagYsame): 

974 yFormat = yFormat + "h" 

975 flags[j] = flag 

976 j = j + 1 

977 if j >= nCoordinates: 

978 break 

979 assert j == nCoordinates, "bad glyph flags" 

980 # unpack raw coordinates, krrrrrr-tching! 

981 xDataLen = struct.calcsize(xFormat) 

982 yDataLen = struct.calcsize(yFormat) 

983 if len(data) - pos - (xDataLen + yDataLen) >= 4: 

984 log.warning( 

985 "too much glyph data: %d excess bytes", 

986 len(data) - pos - (xDataLen + yDataLen), 

987 ) 

988 xCoordinates = struct.unpack(xFormat, data[pos : pos + xDataLen]) 

989 yCoordinates = struct.unpack( 

990 yFormat, data[pos + xDataLen : pos + xDataLen + yDataLen] 

991 ) 

992 return flags, xCoordinates, yCoordinates 

993 

994 def compileComponents(self, glyfTable): 

995 data = b"" 

996 lastcomponent = len(self.components) - 1 

997 more = 1 

998 haveInstructions = 0 

999 for i in range(len(self.components)): 

1000 if i == lastcomponent: 

1001 haveInstructions = hasattr(self, "program") 

1002 more = 0 

1003 compo = self.components[i] 

1004 data = data + compo.compile(more, haveInstructions, glyfTable) 

1005 if haveInstructions: 

1006 instructions = self.program.getBytecode() 

1007 data = data + struct.pack(">h", len(instructions)) + instructions 

1008 return data 

1009 

1010 def compileVarComponents(self, glyfTable): 

1011 return b"".join(c.compile(glyfTable) for c in self.components) 

1012 

1013 def compileCoordinates(self): 

1014 assert len(self.coordinates) == len(self.flags) 

1015 data = [] 

1016 endPtsOfContours = array.array("H", self.endPtsOfContours) 

1017 if sys.byteorder != "big": 

1018 endPtsOfContours.byteswap() 

1019 data.append(endPtsOfContours.tobytes()) 

1020 instructions = self.program.getBytecode() 

1021 data.append(struct.pack(">h", len(instructions))) 

1022 data.append(instructions) 

1023 

1024 deltas = self.coordinates.copy() 

1025 deltas.toInt() 

1026 deltas.absoluteToRelative() 

1027 

1028 # TODO(behdad): Add a configuration option for this? 

1029 deltas = self.compileDeltasGreedy(self.flags, deltas) 

1030 # deltas = self.compileDeltasOptimal(self.flags, deltas) 

1031 

1032 data.extend(deltas) 

1033 return b"".join(data) 

1034 

1035 def compileDeltasGreedy(self, flags, deltas): 

1036 # Implements greedy algorithm for packing coordinate deltas: 

1037 # uses shortest representation one coordinate at a time. 

1038 compressedFlags = bytearray() 

1039 compressedXs = bytearray() 

1040 compressedYs = bytearray() 

1041 lastflag = None 

1042 repeat = 0 

1043 for flag, (x, y) in zip(flags, deltas): 

1044 # Oh, the horrors of TrueType 

1045 # do x 

1046 if x == 0: 

1047 flag = flag | flagXsame 

1048 elif -255 <= x <= 255: 

1049 flag = flag | flagXShort 

1050 if x > 0: 

1051 flag = flag | flagXsame 

1052 else: 

1053 x = -x 

1054 compressedXs.append(x) 

1055 else: 

1056 compressedXs.extend(struct.pack(">h", x)) 

1057 # do y 

1058 if y == 0: 

1059 flag = flag | flagYsame 

1060 elif -255 <= y <= 255: 

1061 flag = flag | flagYShort 

1062 if y > 0: 

1063 flag = flag | flagYsame 

1064 else: 

1065 y = -y 

1066 compressedYs.append(y) 

1067 else: 

1068 compressedYs.extend(struct.pack(">h", y)) 

1069 # handle repeating flags 

1070 if flag == lastflag and repeat != 255: 

1071 repeat = repeat + 1 

1072 if repeat == 1: 

1073 compressedFlags.append(flag) 

1074 else: 

1075 compressedFlags[-2] = flag | flagRepeat 

1076 compressedFlags[-1] = repeat 

1077 else: 

1078 repeat = 0 

1079 compressedFlags.append(flag) 

1080 lastflag = flag 

1081 return (compressedFlags, compressedXs, compressedYs) 

1082 

1083 def compileDeltasOptimal(self, flags, deltas): 

1084 # Implements optimal, dynaic-programming, algorithm for packing coordinate 

1085 # deltas. The savings are negligible :(. 

1086 candidates = [] 

1087 bestTuple = None 

1088 bestCost = 0 

1089 repeat = 0 

1090 for flag, (x, y) in zip(flags, deltas): 

1091 # Oh, the horrors of TrueType 

1092 flag, coordBytes = flagBest(x, y, flag) 

1093 bestCost += 1 + coordBytes 

1094 newCandidates = [ 

1095 (bestCost, bestTuple, flag, coordBytes), 

1096 (bestCost + 1, bestTuple, (flag | flagRepeat), coordBytes), 

1097 ] 

1098 for lastCost, lastTuple, lastFlag, coordBytes in candidates: 

1099 if ( 

1100 lastCost + coordBytes <= bestCost + 1 

1101 and (lastFlag & flagRepeat) 

1102 and (lastFlag < 0xFF00) 

1103 and flagSupports(lastFlag, flag) 

1104 ): 

1105 if (lastFlag & 0xFF) == ( 

1106 flag | flagRepeat 

1107 ) and lastCost == bestCost + 1: 

1108 continue 

1109 newCandidates.append( 

1110 (lastCost + coordBytes, lastTuple, lastFlag + 256, coordBytes) 

1111 ) 

1112 candidates = newCandidates 

1113 bestTuple = min(candidates, key=lambda t: t[0]) 

1114 bestCost = bestTuple[0] 

1115 

1116 flags = [] 

1117 while bestTuple: 

1118 cost, bestTuple, flag, coordBytes = bestTuple 

1119 flags.append(flag) 

1120 flags.reverse() 

1121 

1122 compressedFlags = bytearray() 

1123 compressedXs = bytearray() 

1124 compressedYs = bytearray() 

1125 coords = iter(deltas) 

1126 ff = [] 

1127 for flag in flags: 

1128 repeatCount, flag = flag >> 8, flag & 0xFF 

1129 compressedFlags.append(flag) 

1130 if flag & flagRepeat: 

1131 assert repeatCount > 0 

1132 compressedFlags.append(repeatCount) 

1133 else: 

1134 assert repeatCount == 0 

1135 for i in range(1 + repeatCount): 

1136 x, y = next(coords) 

1137 flagEncodeCoords(flag, x, y, compressedXs, compressedYs) 

1138 ff.append(flag) 

1139 try: 

1140 next(coords) 

1141 raise Exception("internal error") 

1142 except StopIteration: 

1143 pass 

1144 

1145 return (compressedFlags, compressedXs, compressedYs) 

1146 

1147 def recalcBounds(self, glyfTable): 

1148 """Recalculates the bounds of the glyph. 

1149 

1150 Each glyph object stores its bounding box in the 

1151 ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be 

1152 recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds 

1153 must be provided to resolve component bounds. 

1154 """ 

1155 try: 

1156 coords, endPts, flags = self.getCoordinates(glyfTable) 

1157 self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords) 

1158 except NotImplementedError: 

1159 pass 

1160 

1161 def isComposite(self): 

1162 """Test whether a glyph has components""" 

1163 if hasattr(self, "data"): 

1164 return struct.unpack(">h", self.data[:2])[0] == -1 if self.data else False 

1165 else: 

1166 return self.numberOfContours == -1 

1167 

1168 def isVarComposite(self): 

1169 """Test whether a glyph has variable components""" 

1170 if hasattr(self, "data"): 

1171 return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False 

1172 else: 

1173 return self.numberOfContours == -2 

1174 

1175 def getCoordinates(self, glyfTable): 

1176 """Return the coordinates, end points and flags 

1177 

1178 This method returns three values: A :py:class:`GlyphCoordinates` object, 

1179 a list of the indexes of the final points of each contour (allowing you 

1180 to split up the coordinates list into contours) and a list of flags. 

1181 

1182 On simple glyphs, this method returns information from the glyph's own 

1183 contours; on composite glyphs, it "flattens" all components recursively 

1184 to return a list of coordinates representing all the components involved 

1185 in the glyph. 

1186 

1187 To interpret the flags for each point, see the "Simple Glyph Flags" 

1188 section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`. 

1189 """ 

1190 

1191 if self.numberOfContours > 0: 

1192 return self.coordinates, self.endPtsOfContours, self.flags 

1193 elif self.isComposite(): 

1194 # it's a composite 

1195 allCoords = GlyphCoordinates() 

1196 allFlags = bytearray() 

1197 allEndPts = [] 

1198 for compo in self.components: 

1199 g = glyfTable[compo.glyphName] 

1200 try: 

1201 coordinates, endPts, flags = g.getCoordinates(glyfTable) 

1202 except RecursionError: 

1203 raise ttLib.TTLibError( 

1204 "glyph '%s' contains a recursive component reference" 

1205 % compo.glyphName 

1206 ) 

1207 coordinates = GlyphCoordinates(coordinates) 

1208 if hasattr(compo, "firstPt"): 

1209 # component uses two reference points: we apply the transform _before_ 

1210 # computing the offset between the points 

1211 if hasattr(compo, "transform"): 

1212 coordinates.transform(compo.transform) 

1213 x1, y1 = allCoords[compo.firstPt] 

1214 x2, y2 = coordinates[compo.secondPt] 

1215 move = x1 - x2, y1 - y2 

1216 coordinates.translate(move) 

1217 else: 

1218 # component uses XY offsets 

1219 move = compo.x, compo.y 

1220 if not hasattr(compo, "transform"): 

1221 coordinates.translate(move) 

1222 else: 

1223 apple_way = compo.flags & SCALED_COMPONENT_OFFSET 

1224 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET 

1225 assert not (apple_way and ms_way) 

1226 if not (apple_way or ms_way): 

1227 scale_component_offset = ( 

1228 SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file 

1229 ) 

1230 else: 

1231 scale_component_offset = apple_way 

1232 if scale_component_offset: 

1233 # the Apple way: first move, then scale (ie. scale the component offset) 

1234 coordinates.translate(move) 

1235 coordinates.transform(compo.transform) 

1236 else: 

1237 # the MS way: first scale, then move 

1238 coordinates.transform(compo.transform) 

1239 coordinates.translate(move) 

1240 offset = len(allCoords) 

1241 allEndPts.extend(e + offset for e in endPts) 

1242 allCoords.extend(coordinates) 

1243 allFlags.extend(flags) 

1244 return allCoords, allEndPts, allFlags 

1245 elif self.isVarComposite(): 

1246 raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs") 

1247 else: 

1248 return GlyphCoordinates(), [], bytearray() 

1249 

1250 def getComponentNames(self, glyfTable): 

1251 """Returns a list of names of component glyphs used in this glyph 

1252 

1253 This method can be used on simple glyphs (in which case it returns an 

1254 empty list) or composite glyphs. 

1255 """ 

1256 if hasattr(self, "data") and self.isVarComposite(): 

1257 # TODO(VarComposite) Add implementation without expanding glyph 

1258 self.expand(glyfTable) 

1259 

1260 if not hasattr(self, "data"): 

1261 if self.isComposite() or self.isVarComposite(): 

1262 return [c.glyphName for c in self.components] 

1263 else: 

1264 return [] 

1265 

1266 # Extract components without expanding glyph 

1267 

1268 if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: 

1269 return [] # Not composite 

1270 

1271 data = self.data 

1272 i = 10 

1273 components = [] 

1274 more = 1 

1275 while more: 

1276 flags, glyphID = struct.unpack(">HH", data[i : i + 4]) 

1277 i += 4 

1278 flags = int(flags) 

1279 components.append(glyfTable.getGlyphName(int(glyphID))) 

1280 

1281 if flags & ARG_1_AND_2_ARE_WORDS: 

1282 i += 4 

1283 else: 

1284 i += 2 

1285 if flags & WE_HAVE_A_SCALE: 

1286 i += 2 

1287 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1288 i += 4 

1289 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1290 i += 8 

1291 more = flags & MORE_COMPONENTS 

1292 

1293 return components 

1294 

1295 def trim(self, remove_hinting=False): 

1296 """Remove padding and, if requested, hinting, from a glyph. 

1297 This works on both expanded and compacted glyphs, without 

1298 expanding it.""" 

1299 if not hasattr(self, "data"): 

1300 if remove_hinting: 

1301 if self.isComposite(): 

1302 if hasattr(self, "program"): 

1303 del self.program 

1304 elif self.isVarComposite(): 

1305 pass # Doesn't have hinting 

1306 else: 

1307 self.program = ttProgram.Program() 

1308 self.program.fromBytecode([]) 

1309 # No padding to trim. 

1310 return 

1311 if not self.data: 

1312 return 

1313 numContours = struct.unpack(">h", self.data[:2])[0] 

1314 data = bytearray(self.data) 

1315 i = 10 

1316 if numContours >= 0: 

1317 i += 2 * numContours # endPtsOfContours 

1318 nCoordinates = ((data[i - 2] << 8) | data[i - 1]) + 1 

1319 instructionLen = (data[i] << 8) | data[i + 1] 

1320 if remove_hinting: 

1321 # Zero instruction length 

1322 data[i] = data[i + 1] = 0 

1323 i += 2 

1324 if instructionLen: 

1325 # Splice it out 

1326 data = data[:i] + data[i + instructionLen :] 

1327 instructionLen = 0 

1328 else: 

1329 i += 2 + instructionLen 

1330 

1331 coordBytes = 0 

1332 j = 0 

1333 while True: 

1334 flag = data[i] 

1335 i = i + 1 

1336 repeat = 1 

1337 if flag & flagRepeat: 

1338 repeat = data[i] + 1 

1339 i = i + 1 

1340 xBytes = yBytes = 0 

1341 if flag & flagXShort: 

1342 xBytes = 1 

1343 elif not (flag & flagXsame): 

1344 xBytes = 2 

1345 if flag & flagYShort: 

1346 yBytes = 1 

1347 elif not (flag & flagYsame): 

1348 yBytes = 2 

1349 coordBytes += (xBytes + yBytes) * repeat 

1350 j += repeat 

1351 if j >= nCoordinates: 

1352 break 

1353 assert j == nCoordinates, "bad glyph flags" 

1354 i += coordBytes 

1355 # Remove padding 

1356 data = data[:i] 

1357 elif self.isComposite(): 

1358 more = 1 

1359 we_have_instructions = False 

1360 while more: 

1361 flags = (data[i] << 8) | data[i + 1] 

1362 if remove_hinting: 

1363 flags &= ~WE_HAVE_INSTRUCTIONS 

1364 if flags & WE_HAVE_INSTRUCTIONS: 

1365 we_have_instructions = True 

1366 data[i + 0] = flags >> 8 

1367 data[i + 1] = flags & 0xFF 

1368 i += 4 

1369 flags = int(flags) 

1370 

1371 if flags & ARG_1_AND_2_ARE_WORDS: 

1372 i += 4 

1373 else: 

1374 i += 2 

1375 if flags & WE_HAVE_A_SCALE: 

1376 i += 2 

1377 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1378 i += 4 

1379 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1380 i += 8 

1381 more = flags & MORE_COMPONENTS 

1382 if we_have_instructions: 

1383 instructionLen = (data[i] << 8) | data[i + 1] 

1384 i += 2 + instructionLen 

1385 # Remove padding 

1386 data = data[:i] 

1387 elif self.isVarComposite(): 

1388 i = 0 

1389 MIN_SIZE = GlyphVarComponent.MIN_SIZE 

1390 while len(data[i : i + MIN_SIZE]) >= MIN_SIZE: 

1391 size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE]) 

1392 i += size 

1393 data = data[:i] 

1394 

1395 self.data = data 

1396 

1397 def removeHinting(self): 

1398 """Removes TrueType hinting instructions from the glyph.""" 

1399 self.trim(remove_hinting=True) 

1400 

1401 def draw(self, pen, glyfTable, offset=0): 

1402 """Draws the glyph using the supplied pen object. 

1403 

1404 Arguments: 

1405 pen: An object conforming to the pen protocol. 

1406 glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components. 

1407 offset (int): A horizontal offset. If provided, all coordinates are 

1408 translated by this offset. 

1409 """ 

1410 

1411 if self.isComposite(): 

1412 for component in self.components: 

1413 glyphName, transform = component.getComponentInfo() 

1414 pen.addComponent(glyphName, transform) 

1415 return 

1416 

1417 coordinates, endPts, flags = self.getCoordinates(glyfTable) 

1418 if offset: 

1419 coordinates = coordinates.copy() 

1420 coordinates.translate((offset, 0)) 

1421 start = 0 

1422 for end in endPts: 

1423 end = end + 1 

1424 contour = coordinates[start:end] 

1425 cFlags = [flagOnCurve & f for f in flags[start:end]] 

1426 cuFlags = [flagCubic & f for f in flags[start:end]] 

1427 start = end 

1428 if 1 not in cFlags: 

1429 assert all(cuFlags) or not any(cuFlags) 

1430 cubic = all(cuFlags) 

1431 if cubic: 

1432 count = len(contour) 

1433 assert count % 2 == 0, "Odd number of cubic off-curves undefined" 

1434 for i in range(0, count, 2): 

1435 p1 = contour[i] 

1436 p2 = contour[i + 1] 

1437 p4 = contour[i + 2 if i + 2 < count else 0] 

1438 p3 = ((p2[0] + p4[0]) * 0.5, (p2[1] + p4[1]) * 0.5) 

1439 pen.curveTo(p1, p2, p3) 

1440 else: 

1441 # There is not a single on-curve point on the curve, 

1442 # use pen.qCurveTo's special case by specifying None 

1443 # as the on-curve point. 

1444 contour.append(None) 

1445 pen.qCurveTo(*contour) 

1446 else: 

1447 # Shuffle the points so that the contour is guaranteed 

1448 # to *end* in an on-curve point, which we'll use for 

1449 # the moveTo. 

1450 firstOnCurve = cFlags.index(1) + 1 

1451 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 

1452 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 

1453 cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve] 

1454 pen.moveTo(contour[-1]) 

1455 while contour: 

1456 nextOnCurve = cFlags.index(1) + 1 

1457 if nextOnCurve == 1: 

1458 # Skip a final lineTo(), as it is implied by 

1459 # pen.closePath() 

1460 if len(contour) > 1: 

1461 pen.lineTo(contour[0]) 

1462 else: 

1463 cubicFlags = [f for f in cuFlags[: nextOnCurve - 1]] 

1464 assert all(cubicFlags) or not any(cubicFlags) 

1465 cubic = any(cubicFlags) 

1466 if cubic: 

1467 assert all( 

1468 cubicFlags 

1469 ), "Mixed cubic and quadratic segment undefined" 

1470 

1471 count = nextOnCurve 

1472 assert ( 

1473 count >= 3 

1474 ), "At least two cubic off-curve points required" 

1475 assert ( 

1476 count - 1 

1477 ) % 2 == 0, "Odd number of cubic off-curves undefined" 

1478 for i in range(0, count - 3, 2): 

1479 p1 = contour[i] 

1480 p2 = contour[i + 1] 

1481 p4 = contour[i + 2] 

1482 p3 = ((p2[0] + p4[0]) * 0.5, (p2[1] + p4[1]) * 0.5) 

1483 lastOnCurve = p3 

1484 pen.curveTo(p1, p2, p3) 

1485 pen.curveTo(*contour[count - 3 : count]) 

1486 else: 

1487 pen.qCurveTo(*contour[:nextOnCurve]) 

1488 contour = contour[nextOnCurve:] 

1489 cFlags = cFlags[nextOnCurve:] 

1490 cuFlags = cuFlags[nextOnCurve:] 

1491 pen.closePath() 

1492 

1493 def drawPoints(self, pen, glyfTable, offset=0): 

1494 """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(), 

1495 this will not change the point indices. 

1496 """ 

1497 

1498 if self.isComposite(): 

1499 for component in self.components: 

1500 glyphName, transform = component.getComponentInfo() 

1501 pen.addComponent(glyphName, transform) 

1502 return 

1503 

1504 coordinates, endPts, flags = self.getCoordinates(glyfTable) 

1505 if offset: 

1506 coordinates = coordinates.copy() 

1507 coordinates.translate((offset, 0)) 

1508 start = 0 

1509 for end in endPts: 

1510 end = end + 1 

1511 contour = coordinates[start:end] 

1512 cFlags = flags[start:end] 

1513 start = end 

1514 pen.beginPath() 

1515 # Start with the appropriate segment type based on the final segment 

1516 segmentType = "line" if cFlags[-1] == 1 else "qcurve" 

1517 for i, pt in enumerate(contour): 

1518 if cFlags[i] & flagOnCurve == 1: 

1519 pen.addPoint(pt, segmentType=segmentType) 

1520 segmentType = "line" 

1521 else: 

1522 pen.addPoint(pt) 

1523 segmentType = "curve" if cFlags[i] & flagCubic else "qcurve" 

1524 pen.endPath() 

1525 

1526 def __eq__(self, other): 

1527 if type(self) != type(other): 

1528 return NotImplemented 

1529 return self.__dict__ == other.__dict__ 

1530 

1531 def __ne__(self, other): 

1532 result = self.__eq__(other) 

1533 return result if result is NotImplemented else not result 

1534 

1535 

1536def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: 

1537 """Drop impliable on-curve points from the (simple) glyph or glyphs. 

1538 

1539 In TrueType glyf outlines, on-curve points can be implied when they are located at 

1540 the midpoint of the line connecting two consecutive off-curve points. 

1541 

1542 If more than one glyphs are passed, these are assumed to be interpolatable masters 

1543 of the same glyph impliable, and thus only the on-curve points that are impliable 

1544 for all of them will actually be implied. 

1545 Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more 

1546 contours are considered. 

1547 The input glyph(s) is/are modified in-place. 

1548 

1549 Args: 

1550 interpolatable_glyphs: The glyph or glyphs to modify in-place. 

1551 

1552 Returns: 

1553 The set of point indices that were dropped if any. 

1554 

1555 Raises: 

1556 ValueError if simple glyphs are not in fact interpolatable because they have 

1557 different point flags or number of contours. 

1558 

1559 Reference: 

1560 https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html 

1561 """ 

1562 staticAttributes = SimpleNamespace( 

1563 numberOfContours=None, flags=None, endPtsOfContours=None 

1564 ) 

1565 drop = None 

1566 simple_glyphs = [] 

1567 for i, glyph in enumerate(interpolatable_glyphs): 

1568 if glyph.numberOfContours < 1: 

1569 # ignore composite or empty glyphs 

1570 continue 

1571 

1572 for attr in staticAttributes.__dict__: 

1573 expected = getattr(staticAttributes, attr) 

1574 found = getattr(glyph, attr) 

1575 if expected is None: 

1576 setattr(staticAttributes, attr, found) 

1577 elif expected != found: 

1578 raise ValueError( 

1579 f"Incompatible {attr} for glyph at master index {i}: " 

1580 f"expected {expected}, found {found}" 

1581 ) 

1582 

1583 may_drop = set() 

1584 start = 0 

1585 coords = glyph.coordinates 

1586 flags = staticAttributes.flags 

1587 endPtsOfContours = staticAttributes.endPtsOfContours 

1588 for last in endPtsOfContours: 

1589 for i in range(start, last + 1): 

1590 if not (flags[i] & flagOnCurve): 

1591 continue 

1592 prv = i - 1 if i > start else last 

1593 nxt = i + 1 if i < last else start 

1594 if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]: 

1595 continue 

1596 p0 = coords[prv] 

1597 p1 = coords[i] 

1598 p2 = coords[nxt] 

1599 if not math.isclose(p1[0] - p0[0], p2[0] - p1[0]) or not math.isclose( 

1600 p1[1] - p0[1], p2[1] - p1[1] 

1601 ): 

1602 continue 

1603 

1604 may_drop.add(i) 

1605 start = last + 1 

1606 # we only want to drop if ALL interpolatable glyphs have the same implied oncurves 

1607 if drop is None: 

1608 drop = may_drop 

1609 else: 

1610 drop.intersection_update(may_drop) 

1611 

1612 simple_glyphs.append(glyph) 

1613 

1614 if drop: 

1615 # Do the actual dropping 

1616 flags = staticAttributes.flags 

1617 assert flags is not None 

1618 newFlags = array.array( 

1619 "B", (flags[i] for i in range(len(flags)) if i not in drop) 

1620 ) 

1621 

1622 endPts = staticAttributes.endPtsOfContours 

1623 assert endPts is not None 

1624 newEndPts = [] 

1625 i = 0 

1626 delta = 0 

1627 for d in sorted(drop): 

1628 while d > endPts[i]: 

1629 newEndPts.append(endPts[i] - delta) 

1630 i += 1 

1631 delta += 1 

1632 while i < len(endPts): 

1633 newEndPts.append(endPts[i] - delta) 

1634 i += 1 

1635 

1636 for glyph in simple_glyphs: 

1637 coords = glyph.coordinates 

1638 glyph.coordinates = GlyphCoordinates( 

1639 coords[i] for i in range(len(coords)) if i not in drop 

1640 ) 

1641 glyph.flags = newFlags 

1642 glyph.endPtsOfContours = newEndPts 

1643 

1644 return drop if drop is not None else set() 

1645 

1646 

1647class GlyphComponent(object): 

1648 """Represents a component within a composite glyph. 

1649 

1650 The component is represented internally with four attributes: ``glyphName``, 

1651 ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e 

1652 no scaling, reflection, or rotation; only translation), the ``transform`` 

1653 attribute is not present. 

1654 """ 

1655 

1656 # The above documentation is not *completely* true, but is *true enough* because 

1657 # the rare firstPt/lastPt attributes are not totally supported and nobody seems to 

1658 # mind - see below. 

1659 

1660 def __init__(self): 

1661 pass 

1662 

1663 def getComponentInfo(self): 

1664 """Return information about the component 

1665 

1666 This method returns a tuple of two values: the glyph name of the component's 

1667 base glyph, and a transformation matrix. As opposed to accessing the attributes 

1668 directly, ``getComponentInfo`` always returns a six-element tuple of the 

1669 component's transformation matrix, even when the two-by-two ``.transform`` 

1670 matrix is not present. 

1671 """ 

1672 # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement 

1673 # something equivalent in fontTools.objects.glyph (I'd rather not 

1674 # convert it to an absolute offset, since it is valuable information). 

1675 # This method will now raise "AttributeError: x" on glyphs that use 

1676 # this TT feature. 

1677 if hasattr(self, "transform"): 

1678 [[xx, xy], [yx, yy]] = self.transform 

1679 trans = (xx, xy, yx, yy, self.x, self.y) 

1680 else: 

1681 trans = (1, 0, 0, 1, self.x, self.y) 

1682 return self.glyphName, trans 

1683 

1684 def decompile(self, data, glyfTable): 

1685 flags, glyphID = struct.unpack(">HH", data[:4]) 

1686 self.flags = int(flags) 

1687 glyphID = int(glyphID) 

1688 self.glyphName = glyfTable.getGlyphName(int(glyphID)) 

1689 data = data[4:] 

1690 

1691 if self.flags & ARG_1_AND_2_ARE_WORDS: 

1692 if self.flags & ARGS_ARE_XY_VALUES: 

1693 self.x, self.y = struct.unpack(">hh", data[:4]) 

1694 else: 

1695 x, y = struct.unpack(">HH", data[:4]) 

1696 self.firstPt, self.secondPt = int(x), int(y) 

1697 data = data[4:] 

1698 else: 

1699 if self.flags & ARGS_ARE_XY_VALUES: 

1700 self.x, self.y = struct.unpack(">bb", data[:2]) 

1701 else: 

1702 x, y = struct.unpack(">BB", data[:2]) 

1703 self.firstPt, self.secondPt = int(x), int(y) 

1704 data = data[2:] 

1705 

1706 if self.flags & WE_HAVE_A_SCALE: 

1707 (scale,) = struct.unpack(">h", data[:2]) 

1708 self.transform = [ 

1709 [fi2fl(scale, 14), 0], 

1710 [0, fi2fl(scale, 14)], 

1711 ] # fixed 2.14 

1712 data = data[2:] 

1713 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1714 xscale, yscale = struct.unpack(">hh", data[:4]) 

1715 self.transform = [ 

1716 [fi2fl(xscale, 14), 0], 

1717 [0, fi2fl(yscale, 14)], 

1718 ] # fixed 2.14 

1719 data = data[4:] 

1720 elif self.flags & WE_HAVE_A_TWO_BY_TWO: 

1721 (xscale, scale01, scale10, yscale) = struct.unpack(">hhhh", data[:8]) 

1722 self.transform = [ 

1723 [fi2fl(xscale, 14), fi2fl(scale01, 14)], 

1724 [fi2fl(scale10, 14), fi2fl(yscale, 14)], 

1725 ] # fixed 2.14 

1726 data = data[8:] 

1727 more = self.flags & MORE_COMPONENTS 

1728 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS 

1729 self.flags = self.flags & ( 

1730 ROUND_XY_TO_GRID 

1731 | USE_MY_METRICS 

1732 | SCALED_COMPONENT_OFFSET 

1733 | UNSCALED_COMPONENT_OFFSET 

1734 | NON_OVERLAPPING 

1735 | OVERLAP_COMPOUND 

1736 ) 

1737 return more, haveInstructions, data 

1738 

1739 def compile(self, more, haveInstructions, glyfTable): 

1740 data = b"" 

1741 

1742 # reset all flags we will calculate ourselves 

1743 flags = self.flags & ( 

1744 ROUND_XY_TO_GRID 

1745 | USE_MY_METRICS 

1746 | SCALED_COMPONENT_OFFSET 

1747 | UNSCALED_COMPONENT_OFFSET 

1748 | NON_OVERLAPPING 

1749 | OVERLAP_COMPOUND 

1750 ) 

1751 if more: 

1752 flags = flags | MORE_COMPONENTS 

1753 if haveInstructions: 

1754 flags = flags | WE_HAVE_INSTRUCTIONS 

1755 

1756 if hasattr(self, "firstPt"): 

1757 if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255): 

1758 data = data + struct.pack(">BB", self.firstPt, self.secondPt) 

1759 else: 

1760 data = data + struct.pack(">HH", self.firstPt, self.secondPt) 

1761 flags = flags | ARG_1_AND_2_ARE_WORDS 

1762 else: 

1763 x = otRound(self.x) 

1764 y = otRound(self.y) 

1765 flags = flags | ARGS_ARE_XY_VALUES 

1766 if (-128 <= x <= 127) and (-128 <= y <= 127): 

1767 data = data + struct.pack(">bb", x, y) 

1768 else: 

1769 data = data + struct.pack(">hh", x, y) 

1770 flags = flags | ARG_1_AND_2_ARE_WORDS 

1771 

1772 if hasattr(self, "transform"): 

1773 transform = [[fl2fi(x, 14) for x in row] for row in self.transform] 

1774 if transform[0][1] or transform[1][0]: 

1775 flags = flags | WE_HAVE_A_TWO_BY_TWO 

1776 data = data + struct.pack( 

1777 ">hhhh", 

1778 transform[0][0], 

1779 transform[0][1], 

1780 transform[1][0], 

1781 transform[1][1], 

1782 ) 

1783 elif transform[0][0] != transform[1][1]: 

1784 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE 

1785 data = data + struct.pack(">hh", transform[0][0], transform[1][1]) 

1786 else: 

1787 flags = flags | WE_HAVE_A_SCALE 

1788 data = data + struct.pack(">h", transform[0][0]) 

1789 

1790 glyphID = glyfTable.getGlyphID(self.glyphName) 

1791 return struct.pack(">HH", flags, glyphID) + data 

1792 

1793 def toXML(self, writer, ttFont): 

1794 attrs = [("glyphName", self.glyphName)] 

1795 if not hasattr(self, "firstPt"): 

1796 attrs = attrs + [("x", self.x), ("y", self.y)] 

1797 else: 

1798 attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)] 

1799 

1800 if hasattr(self, "transform"): 

1801 transform = self.transform 

1802 if transform[0][1] or transform[1][0]: 

1803 attrs = attrs + [ 

1804 ("scalex", fl2str(transform[0][0], 14)), 

1805 ("scale01", fl2str(transform[0][1], 14)), 

1806 ("scale10", fl2str(transform[1][0], 14)), 

1807 ("scaley", fl2str(transform[1][1], 14)), 

1808 ] 

1809 elif transform[0][0] != transform[1][1]: 

1810 attrs = attrs + [ 

1811 ("scalex", fl2str(transform[0][0], 14)), 

1812 ("scaley", fl2str(transform[1][1], 14)), 

1813 ] 

1814 else: 

1815 attrs = attrs + [("scale", fl2str(transform[0][0], 14))] 

1816 attrs = attrs + [("flags", hex(self.flags))] 

1817 writer.simpletag("component", attrs) 

1818 writer.newline() 

1819 

1820 def fromXML(self, name, attrs, content, ttFont): 

1821 self.glyphName = attrs["glyphName"] 

1822 if "firstPt" in attrs: 

1823 self.firstPt = safeEval(attrs["firstPt"]) 

1824 self.secondPt = safeEval(attrs["secondPt"]) 

1825 else: 

1826 self.x = safeEval(attrs["x"]) 

1827 self.y = safeEval(attrs["y"]) 

1828 if "scale01" in attrs: 

1829 scalex = str2fl(attrs["scalex"], 14) 

1830 scale01 = str2fl(attrs["scale01"], 14) 

1831 scale10 = str2fl(attrs["scale10"], 14) 

1832 scaley = str2fl(attrs["scaley"], 14) 

1833 self.transform = [[scalex, scale01], [scale10, scaley]] 

1834 elif "scalex" in attrs: 

1835 scalex = str2fl(attrs["scalex"], 14) 

1836 scaley = str2fl(attrs["scaley"], 14) 

1837 self.transform = [[scalex, 0], [0, scaley]] 

1838 elif "scale" in attrs: 

1839 scale = str2fl(attrs["scale"], 14) 

1840 self.transform = [[scale, 0], [0, scale]] 

1841 self.flags = safeEval(attrs["flags"]) 

1842 

1843 def __eq__(self, other): 

1844 if type(self) != type(other): 

1845 return NotImplemented 

1846 return self.__dict__ == other.__dict__ 

1847 

1848 def __ne__(self, other): 

1849 result = self.__eq__(other) 

1850 return result if result is NotImplemented else not result 

1851 

1852 

1853# 

1854# Variable Composite glyphs 

1855# https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md 

1856# 

1857 

1858 

1859class VarComponentFlags(IntFlag): 

1860 USE_MY_METRICS = 0x0001 

1861 AXIS_INDICES_ARE_SHORT = 0x0002 

1862 UNIFORM_SCALE = 0x0004 

1863 HAVE_TRANSLATE_X = 0x0008 

1864 HAVE_TRANSLATE_Y = 0x0010 

1865 HAVE_ROTATION = 0x0020 

1866 HAVE_SCALE_X = 0x0040 

1867 HAVE_SCALE_Y = 0x0080 

1868 HAVE_SKEW_X = 0x0100 

1869 HAVE_SKEW_Y = 0x0200 

1870 HAVE_TCENTER_X = 0x0400 

1871 HAVE_TCENTER_Y = 0x0800 

1872 GID_IS_24BIT = 0x1000 

1873 AXES_HAVE_VARIATION = 0x2000 

1874 RESET_UNSPECIFIED_AXES = 0x4000 

1875 

1876 

1877VarComponentTransformMappingValues = namedtuple( 

1878 "VarComponentTransformMappingValues", 

1879 ["flag", "fractionalBits", "scale", "defaultValue"], 

1880) 

1881 

1882VAR_COMPONENT_TRANSFORM_MAPPING = { 

1883 "translateX": VarComponentTransformMappingValues( 

1884 VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 

1885 ), 

1886 "translateY": VarComponentTransformMappingValues( 

1887 VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 

1888 ), 

1889 "rotation": VarComponentTransformMappingValues( 

1890 VarComponentFlags.HAVE_ROTATION, 12, 180, 0 

1891 ), 

1892 "scaleX": VarComponentTransformMappingValues( 

1893 VarComponentFlags.HAVE_SCALE_X, 10, 1, 1 

1894 ), 

1895 "scaleY": VarComponentTransformMappingValues( 

1896 VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1 

1897 ), 

1898 "skewX": VarComponentTransformMappingValues( 

1899 VarComponentFlags.HAVE_SKEW_X, 12, -180, 0 

1900 ), 

1901 "skewY": VarComponentTransformMappingValues( 

1902 VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0 

1903 ), 

1904 "tCenterX": VarComponentTransformMappingValues( 

1905 VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0 

1906 ), 

1907 "tCenterY": VarComponentTransformMappingValues( 

1908 VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0 

1909 ), 

1910} 

1911 

1912 

1913class GlyphVarComponent(object): 

1914 

1915 MIN_SIZE = 5 

1916 

1917 def __init__(self): 

1918 self.location = {} 

1919 self.transform = DecomposedTransform() 

1920 

1921 @staticmethod 

1922 def getSize(data): 

1923 size = 5 

1924 flags = struct.unpack(">H", data[:2])[0] 

1925 numAxes = int(data[2]) 

1926 

1927 if flags & VarComponentFlags.GID_IS_24BIT: 

1928 size += 1 

1929 

1930 size += numAxes 

1931 if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: 

1932 size += 2 * numAxes 

1933 else: 

1934 axisIndices = array.array("B", data[:numAxes]) 

1935 size += numAxes 

1936 

1937 for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 

1938 if flags & mapping_values.flag: 

1939 size += 2 

1940 

1941 return size 

1942 

1943 def decompile(self, data, glyfTable): 

1944 flags = struct.unpack(">H", data[:2])[0] 

1945 self.flags = int(flags) 

1946 data = data[2:] 

1947 

1948 numAxes = int(data[0]) 

1949 data = data[1:] 

1950 

1951 if flags & VarComponentFlags.GID_IS_24BIT: 

1952 glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0]) 

1953 data = data[3:] 

1954 flags ^= VarComponentFlags.GID_IS_24BIT 

1955 else: 

1956 glyphID = int(struct.unpack(">H", data[:2])[0]) 

1957 data = data[2:] 

1958 self.glyphName = glyfTable.getGlyphName(int(glyphID)) 

1959 

1960 if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: 

1961 axisIndices = array.array("H", data[: 2 * numAxes]) 

1962 if sys.byteorder != "big": 

1963 axisIndices.byteswap() 

1964 data = data[2 * numAxes :] 

1965 flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT 

1966 else: 

1967 axisIndices = array.array("B", data[:numAxes]) 

1968 data = data[numAxes:] 

1969 assert len(axisIndices) == numAxes 

1970 axisIndices = list(axisIndices) 

1971 

1972 axisValues = array.array("h", data[: 2 * numAxes]) 

1973 if sys.byteorder != "big": 

1974 axisValues.byteswap() 

1975 data = data[2 * numAxes :] 

1976 assert len(axisValues) == numAxes 

1977 axisValues = [fi2fl(v, 14) for v in axisValues] 

1978 

1979 self.location = { 

1980 glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues) 

1981 } 

1982 

1983 def read_transform_component(data, values): 

1984 if flags & values.flag: 

1985 return ( 

1986 data[2:], 

1987 fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits) 

1988 * values.scale, 

1989 ) 

1990 else: 

1991 return data, values.defaultValue 

1992 

1993 for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 

1994 data, value = read_transform_component(data, mapping_values) 

1995 setattr(self.transform, attr_name, value) 

1996 

1997 if flags & VarComponentFlags.UNIFORM_SCALE: 

1998 if flags & VarComponentFlags.HAVE_SCALE_X and not ( 

1999 flags & VarComponentFlags.HAVE_SCALE_Y 

2000 ): 

2001 self.transform.scaleY = self.transform.scaleX 

2002 flags |= VarComponentFlags.HAVE_SCALE_Y 

2003 flags ^= VarComponentFlags.UNIFORM_SCALE 

2004 

2005 return data 

2006 

2007 def compile(self, glyfTable): 

2008 data = b"" 

2009 

2010 if not hasattr(self, "flags"): 

2011 flags = 0 

2012 # Calculate optimal transform component flags 

2013 for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 

2014 value = getattr(self.transform, attr_name) 

2015 if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi( 

2016 mapping.defaultValue / mapping.scale, mapping.fractionalBits 

2017 ): 

2018 flags |= mapping.flag 

2019 else: 

2020 flags = self.flags 

2021 

2022 if ( 

2023 flags & VarComponentFlags.HAVE_SCALE_X 

2024 and flags & VarComponentFlags.HAVE_SCALE_Y 

2025 and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10) 

2026 ): 

2027 flags |= VarComponentFlags.UNIFORM_SCALE 

2028 flags ^= VarComponentFlags.HAVE_SCALE_Y 

2029 

2030 numAxes = len(self.location) 

2031 

2032 data = data + struct.pack(">B", numAxes) 

2033 

2034 glyphID = glyfTable.getGlyphID(self.glyphName) 

2035 if glyphID > 65535: 

2036 flags |= VarComponentFlags.GID_IS_24BIT 

2037 data = data + struct.pack(">L", glyphID)[1:] 

2038 else: 

2039 data = data + struct.pack(">H", glyphID) 

2040 

2041 axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()] 

2042 if all(a <= 255 for a in axisIndices): 

2043 axisIndices = array.array("B", axisIndices) 

2044 else: 

2045 axisIndices = array.array("H", axisIndices) 

2046 if sys.byteorder != "big": 

2047 axisIndices.byteswap() 

2048 flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT 

2049 data = data + bytes(axisIndices) 

2050 

2051 axisValues = self.location.values() 

2052 axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues)) 

2053 if sys.byteorder != "big": 

2054 axisValues.byteswap() 

2055 data = data + bytes(axisValues) 

2056 

2057 def write_transform_component(data, value, values): 

2058 if flags & values.flag: 

2059 return data + struct.pack( 

2060 ">h", fl2fi(value / values.scale, values.fractionalBits) 

2061 ) 

2062 else: 

2063 return data 

2064 

2065 for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 

2066 value = getattr(self.transform, attr_name) 

2067 data = write_transform_component(data, value, mapping_values) 

2068 

2069 return struct.pack(">H", flags) + data 

2070 

2071 def toXML(self, writer, ttFont): 

2072 attrs = [("glyphName", self.glyphName)] 

2073 

2074 if hasattr(self, "flags"): 

2075 attrs = attrs + [("flags", hex(self.flags))] 

2076 

2077 for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 

2078 v = getattr(self.transform, attr_name) 

2079 if v != mapping.defaultValue: 

2080 attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) 

2081 

2082 writer.begintag("varComponent", attrs) 

2083 writer.newline() 

2084 

2085 writer.begintag("location") 

2086 writer.newline() 

2087 for tag, v in self.location.items(): 

2088 writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))]) 

2089 writer.newline() 

2090 writer.endtag("location") 

2091 writer.newline() 

2092 

2093 writer.endtag("varComponent") 

2094 writer.newline() 

2095 

2096 def fromXML(self, name, attrs, content, ttFont): 

2097 self.glyphName = attrs["glyphName"] 

2098 

2099 if "flags" in attrs: 

2100 self.flags = safeEval(attrs["flags"]) 

2101 

2102 for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 

2103 if attr_name not in attrs: 

2104 continue 

2105 v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits) 

2106 setattr(self.transform, attr_name, v) 

2107 

2108 for c in content: 

2109 if not isinstance(c, tuple): 

2110 continue 

2111 name, attrs, content = c 

2112 if name != "location": 

2113 continue 

2114 for c in content: 

2115 if not isinstance(c, tuple): 

2116 continue 

2117 name, attrs, content = c 

2118 assert name == "axis" 

2119 assert not content 

2120 self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14) 

2121 

2122 def getPointCount(self): 

2123 assert hasattr(self, "flags"), "VarComponent with variations must have flags" 

2124 

2125 count = 0 

2126 

2127 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 

2128 count += len(self.location) 

2129 

2130 if self.flags & ( 

2131 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 

2132 ): 

2133 count += 1 

2134 if self.flags & VarComponentFlags.HAVE_ROTATION: 

2135 count += 1 

2136 if self.flags & ( 

2137 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 

2138 ): 

2139 count += 1 

2140 if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): 

2141 count += 1 

2142 if self.flags & ( 

2143 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 

2144 ): 

2145 count += 1 

2146 

2147 return count 

2148 

2149 def getCoordinatesAndControls(self): 

2150 

2151 coords = [] 

2152 controls = [] 

2153 

2154 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 

2155 for tag, v in self.location.items(): 

2156 controls.append(tag) 

2157 coords.append((fl2fi(v, 14), 0)) 

2158 

2159 if self.flags & ( 

2160 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 

2161 ): 

2162 controls.append("translate") 

2163 coords.append((self.transform.translateX, self.transform.translateY)) 

2164 if self.flags & VarComponentFlags.HAVE_ROTATION: 

2165 controls.append("rotation") 

2166 coords.append((fl2fi(self.transform.rotation / 180, 12), 0)) 

2167 if self.flags & ( 

2168 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 

2169 ): 

2170 controls.append("scale") 

2171 coords.append( 

2172 (fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10)) 

2173 ) 

2174 if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): 

2175 controls.append("skew") 

2176 coords.append( 

2177 ( 

2178 fl2fi(self.transform.skewX / -180, 12), 

2179 fl2fi(self.transform.skewY / 180, 12), 

2180 ) 

2181 ) 

2182 if self.flags & ( 

2183 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 

2184 ): 

2185 controls.append("tCenter") 

2186 coords.append((self.transform.tCenterX, self.transform.tCenterY)) 

2187 

2188 return coords, controls 

2189 

2190 def setCoordinates(self, coords): 

2191 i = 0 

2192 

2193 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 

2194 newLocation = {} 

2195 for tag in self.location: 

2196 newLocation[tag] = fi2fl(coords[i][0], 14) 

2197 i += 1 

2198 self.location = newLocation 

2199 

2200 self.transform = DecomposedTransform() 

2201 if self.flags & ( 

2202 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 

2203 ): 

2204 self.transform.translateX, self.transform.translateY = coords[i] 

2205 i += 1 

2206 if self.flags & VarComponentFlags.HAVE_ROTATION: 

2207 self.transform.rotation = fi2fl(coords[i][0], 12) * 180 

2208 i += 1 

2209 if self.flags & ( 

2210 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 

2211 ): 

2212 self.transform.scaleX, self.transform.scaleY = fi2fl( 

2213 coords[i][0], 10 

2214 ), fi2fl(coords[i][1], 10) 

2215 i += 1 

2216 if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): 

2217 self.transform.skewX, self.transform.skewY = ( 

2218 fi2fl(coords[i][0], 12) * -180, 

2219 fi2fl(coords[i][1], 12) * 180, 

2220 ) 

2221 i += 1 

2222 if self.flags & ( 

2223 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 

2224 ): 

2225 self.transform.tCenterX, self.transform.tCenterY = coords[i] 

2226 i += 1 

2227 

2228 return coords[i:] 

2229 

2230 def __eq__(self, other): 

2231 if type(self) != type(other): 

2232 return NotImplemented 

2233 return self.__dict__ == other.__dict__ 

2234 

2235 def __ne__(self, other): 

2236 result = self.__eq__(other) 

2237 return result if result is NotImplemented else not result 

2238 

2239 

2240class GlyphCoordinates(object): 

2241 """A list of glyph coordinates. 

2242 

2243 Unlike an ordinary list, this is a numpy-like matrix object which supports 

2244 matrix addition, scalar multiplication and other operations described below. 

2245 """ 

2246 

2247 def __init__(self, iterable=[]): 

2248 self._a = array.array("d") 

2249 self.extend(iterable) 

2250 

2251 @property 

2252 def array(self): 

2253 """Returns the underlying array of coordinates""" 

2254 return self._a 

2255 

2256 @staticmethod 

2257 def zeros(count): 

2258 """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)""" 

2259 g = GlyphCoordinates() 

2260 g._a.frombytes(bytes(count * 2 * g._a.itemsize)) 

2261 return g 

2262 

2263 def copy(self): 

2264 """Creates a new ``GlyphCoordinates`` object which is a copy of the current one.""" 

2265 c = GlyphCoordinates() 

2266 c._a.extend(self._a) 

2267 return c 

2268 

2269 def __len__(self): 

2270 """Returns the number of coordinates in the array.""" 

2271 return len(self._a) // 2 

2272 

2273 def __getitem__(self, k): 

2274 """Returns a two element tuple (x,y)""" 

2275 if isinstance(k, slice): 

2276 indices = range(*k.indices(len(self))) 

2277 return [self[i] for i in indices] 

2278 a = self._a 

2279 x = a[2 * k] 

2280 y = a[2 * k + 1] 

2281 return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y) 

2282 

2283 def __setitem__(self, k, v): 

2284 """Sets a point's coordinates to a two element tuple (x,y)""" 

2285 if isinstance(k, slice): 

2286 indices = range(*k.indices(len(self))) 

2287 # XXX This only works if len(v) == len(indices) 

2288 for j, i in enumerate(indices): 

2289 self[i] = v[j] 

2290 return 

2291 self._a[2 * k], self._a[2 * k + 1] = v 

2292 

2293 def __delitem__(self, i): 

2294 """Removes a point from the list""" 

2295 i = (2 * i) % len(self._a) 

2296 del self._a[i] 

2297 del self._a[i] 

2298 

2299 def __repr__(self): 

2300 return "GlyphCoordinates([" + ",".join(str(c) for c in self) + "])" 

2301 

2302 def append(self, p): 

2303 self._a.extend(tuple(p)) 

2304 

2305 def extend(self, iterable): 

2306 for p in iterable: 

2307 self._a.extend(p) 

2308 

2309 def toInt(self, *, round=otRound): 

2310 a = self._a 

2311 for i in range(len(a)): 

2312 a[i] = round(a[i]) 

2313 

2314 def relativeToAbsolute(self): 

2315 a = self._a 

2316 x, y = 0, 0 

2317 for i in range(0, len(a), 2): 

2318 a[i] = x = a[i] + x 

2319 a[i + 1] = y = a[i + 1] + y 

2320 

2321 def absoluteToRelative(self): 

2322 a = self._a 

2323 x, y = 0, 0 

2324 for i in range(0, len(a), 2): 

2325 nx = a[i] 

2326 ny = a[i + 1] 

2327 a[i] = nx - x 

2328 a[i + 1] = ny - y 

2329 x = nx 

2330 y = ny 

2331 

2332 def translate(self, p): 

2333 """ 

2334 >>> GlyphCoordinates([(1,2)]).translate((.5,0)) 

2335 """ 

2336 x, y = p 

2337 if x == 0 and y == 0: 

2338 return 

2339 a = self._a 

2340 for i in range(0, len(a), 2): 

2341 a[i] += x 

2342 a[i + 1] += y 

2343 

2344 def scale(self, p): 

2345 """ 

2346 >>> GlyphCoordinates([(1,2)]).scale((.5,0)) 

2347 """ 

2348 x, y = p 

2349 if x == 1 and y == 1: 

2350 return 

2351 a = self._a 

2352 for i in range(0, len(a), 2): 

2353 a[i] *= x 

2354 a[i + 1] *= y 

2355 

2356 def transform(self, t): 

2357 """ 

2358 >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5))) 

2359 """ 

2360 a = self._a 

2361 for i in range(0, len(a), 2): 

2362 x = a[i] 

2363 y = a[i + 1] 

2364 px = x * t[0][0] + y * t[1][0] 

2365 py = x * t[0][1] + y * t[1][1] 

2366 a[i] = px 

2367 a[i + 1] = py 

2368 

2369 def __eq__(self, other): 

2370 """ 

2371 >>> g = GlyphCoordinates([(1,2)]) 

2372 >>> g2 = GlyphCoordinates([(1.0,2)]) 

2373 >>> g3 = GlyphCoordinates([(1.5,2)]) 

2374 >>> g == g2 

2375 True 

2376 >>> g == g3 

2377 False 

2378 >>> g2 == g3 

2379 False 

2380 """ 

2381 if type(self) != type(other): 

2382 return NotImplemented 

2383 return self._a == other._a 

2384 

2385 def __ne__(self, other): 

2386 """ 

2387 >>> g = GlyphCoordinates([(1,2)]) 

2388 >>> g2 = GlyphCoordinates([(1.0,2)]) 

2389 >>> g3 = GlyphCoordinates([(1.5,2)]) 

2390 >>> g != g2 

2391 False 

2392 >>> g != g3 

2393 True 

2394 >>> g2 != g3 

2395 True 

2396 """ 

2397 result = self.__eq__(other) 

2398 return result if result is NotImplemented else not result 

2399 

2400 # Math operations 

2401 

2402 def __pos__(self): 

2403 """ 

2404 >>> g = GlyphCoordinates([(1,2)]) 

2405 >>> g 

2406 GlyphCoordinates([(1, 2)]) 

2407 >>> g2 = +g 

2408 >>> g2 

2409 GlyphCoordinates([(1, 2)]) 

2410 >>> g2.translate((1,0)) 

2411 >>> g2 

2412 GlyphCoordinates([(2, 2)]) 

2413 >>> g 

2414 GlyphCoordinates([(1, 2)]) 

2415 """ 

2416 return self.copy() 

2417 

2418 def __neg__(self): 

2419 """ 

2420 >>> g = GlyphCoordinates([(1,2)]) 

2421 >>> g 

2422 GlyphCoordinates([(1, 2)]) 

2423 >>> g2 = -g 

2424 >>> g2 

2425 GlyphCoordinates([(-1, -2)]) 

2426 >>> g 

2427 GlyphCoordinates([(1, 2)]) 

2428 """ 

2429 r = self.copy() 

2430 a = r._a 

2431 for i in range(len(a)): 

2432 a[i] = -a[i] 

2433 return r 

2434 

2435 def __round__(self, *, round=otRound): 

2436 r = self.copy() 

2437 r.toInt(round=round) 

2438 return r 

2439 

2440 def __add__(self, other): 

2441 return self.copy().__iadd__(other) 

2442 

2443 def __sub__(self, other): 

2444 return self.copy().__isub__(other) 

2445 

2446 def __mul__(self, other): 

2447 return self.copy().__imul__(other) 

2448 

2449 def __truediv__(self, other): 

2450 return self.copy().__itruediv__(other) 

2451 

2452 __radd__ = __add__ 

2453 __rmul__ = __mul__ 

2454 

2455 def __rsub__(self, other): 

2456 return other + (-self) 

2457 

2458 def __iadd__(self, other): 

2459 """ 

2460 >>> g = GlyphCoordinates([(1,2)]) 

2461 >>> g += (.5,0) 

2462 >>> g 

2463 GlyphCoordinates([(1.5, 2)]) 

2464 >>> g2 = GlyphCoordinates([(3,4)]) 

2465 >>> g += g2 

2466 >>> g 

2467 GlyphCoordinates([(4.5, 6)]) 

2468 """ 

2469 if isinstance(other, tuple): 

2470 assert len(other) == 2 

2471 self.translate(other) 

2472 return self 

2473 if isinstance(other, GlyphCoordinates): 

2474 other = other._a 

2475 a = self._a 

2476 assert len(a) == len(other) 

2477 for i in range(len(a)): 

2478 a[i] += other[i] 

2479 return self 

2480 return NotImplemented 

2481 

2482 def __isub__(self, other): 

2483 """ 

2484 >>> g = GlyphCoordinates([(1,2)]) 

2485 >>> g -= (.5,0) 

2486 >>> g 

2487 GlyphCoordinates([(0.5, 2)]) 

2488 >>> g2 = GlyphCoordinates([(3,4)]) 

2489 >>> g -= g2 

2490 >>> g 

2491 GlyphCoordinates([(-2.5, -2)]) 

2492 """ 

2493 if isinstance(other, tuple): 

2494 assert len(other) == 2 

2495 self.translate((-other[0], -other[1])) 

2496 return self 

2497 if isinstance(other, GlyphCoordinates): 

2498 other = other._a 

2499 a = self._a 

2500 assert len(a) == len(other) 

2501 for i in range(len(a)): 

2502 a[i] -= other[i] 

2503 return self 

2504 return NotImplemented 

2505 

2506 def __imul__(self, other): 

2507 """ 

2508 >>> g = GlyphCoordinates([(1,2)]) 

2509 >>> g *= (2,.5) 

2510 >>> g *= 2 

2511 >>> g 

2512 GlyphCoordinates([(4, 2)]) 

2513 >>> g = GlyphCoordinates([(1,2)]) 

2514 >>> g *= 2 

2515 >>> g 

2516 GlyphCoordinates([(2, 4)]) 

2517 """ 

2518 if isinstance(other, tuple): 

2519 assert len(other) == 2 

2520 self.scale(other) 

2521 return self 

2522 if isinstance(other, Number): 

2523 if other == 1: 

2524 return self 

2525 a = self._a 

2526 for i in range(len(a)): 

2527 a[i] *= other 

2528 return self 

2529 return NotImplemented 

2530 

2531 def __itruediv__(self, other): 

2532 """ 

2533 >>> g = GlyphCoordinates([(1,3)]) 

2534 >>> g /= (.5,1.5) 

2535 >>> g /= 2 

2536 >>> g 

2537 GlyphCoordinates([(1, 1)]) 

2538 """ 

2539 if isinstance(other, Number): 

2540 other = (other, other) 

2541 if isinstance(other, tuple): 

2542 if other == (1, 1): 

2543 return self 

2544 assert len(other) == 2 

2545 self.scale((1.0 / other[0], 1.0 / other[1])) 

2546 return self 

2547 return NotImplemented 

2548 

2549 def __bool__(self): 

2550 """ 

2551 >>> g = GlyphCoordinates([]) 

2552 >>> bool(g) 

2553 False 

2554 >>> g = GlyphCoordinates([(0,0), (0.,0)]) 

2555 >>> bool(g) 

2556 True 

2557 >>> g = GlyphCoordinates([(0,0), (1,0)]) 

2558 >>> bool(g) 

2559 True 

2560 >>> g = GlyphCoordinates([(0,.5), (0,0)]) 

2561 >>> bool(g) 

2562 True 

2563 """ 

2564 return bool(self._a) 

2565 

2566 __nonzero__ = __bool__ 

2567 

2568 

2569if __name__ == "__main__": 

2570 import doctest, sys 

2571 

2572 sys.exit(doctest.testmod().failed)