Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/_g_l_y_f.py: 13%

1639 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

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 updateBounds, 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) 

17from fontTools.misc.roundTools import noRound, otRound 

18from fontTools.misc.vector import Vector 

19from numbers import Number 

20from . import DefaultTable 

21from . import ttProgram 

22import sys 

23import struct 

24import array 

25import logging 

26import math 

27import os 

28from fontTools.misc import xmlWriter 

29from fontTools.misc.filenames import userNameToFileName 

30from fontTools.misc.loggingTools import deprecateFunction 

31from enum import IntFlag 

32from functools import partial 

33from types import SimpleNamespace 

34from typing import Set 

35 

36log = logging.getLogger(__name__) 

37 

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

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

40# when glyf is written to separate files. 

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

42 

43# 

44# The Apple and MS rasterizers behave differently for 

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

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

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

48# 

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

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

51# (eg. Charcoal)... 

52# 

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

54 

55 

56class table__g_l_y_f(DefaultTable.DefaultTable): 

57 """Glyph Data Table 

58 

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

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

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

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

63 

64 >> from fontTools.pens.boundsPen import BoundsPen 

65 >> glyphset = font.getGlyphSet() 

66 >> bp = BoundsPen(glyphset) 

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

68 >> bp.bounds 

69 (19, 0, 633, 716) 

70 

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

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

73 :py:class:`Glyph` objects:: 

74 

75 >> glyf = font["glyf"] 

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

77 2 

78 

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

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

81 

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

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

84 

85 """ 

86 

87 dependencies = ["fvar"] 

88 

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

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

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

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

93 padding = 1 

94 

95 def decompile(self, data, ttFont): 

96 self.axisTags = ( 

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

98 ) 

99 loca = ttFont["loca"] 

100 pos = int(loca[0]) 

101 nextPos = 0 

102 noname = 0 

103 self.glyphs = {} 

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

105 self._reverseGlyphOrder = {} 

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

107 try: 

108 glyphName = glyphOrder[i] 

109 except IndexError: 

110 noname = noname + 1 

111 glyphName = "ttxautoglyph%s" % i 

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

113 glyphdata = data[pos:nextPos] 

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

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

116 glyph = Glyph(glyphdata) 

117 self.glyphs[glyphName] = glyph 

118 pos = nextPos 

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

120 log.warning( 

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

122 nextPos, 

123 len(data), 

124 ) 

125 if noname: 

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

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

128 self.ensureDecompiled() 

129 

130 def ensureDecompiled(self, recurse=False): 

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

132 # ensureDecompiled across the library. 

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

134 glyph.expand(self) 

135 

136 def compile(self, ttFont): 

137 self.axisTags = ( 

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

139 ) 

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

141 self.glyphOrder = ttFont.getGlyphOrder() 

142 padding = self.padding 

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

144 locations = [] 

145 currentLocation = 0 

146 dataList = [] 

147 recalcBBoxes = ttFont.recalcBBoxes 

148 boundsDone = set() 

149 for glyphName in self.glyphOrder: 

150 glyph = self.glyphs[glyphName] 

151 glyphData = glyph.compile(self, recalcBBoxes, boundsDone=boundsDone) 

152 if padding > 1: 

153 glyphData = pad(glyphData, size=padding) 

154 locations.append(currentLocation) 

155 currentLocation = currentLocation + len(glyphData) 

156 dataList.append(glyphData) 

157 locations.append(currentLocation) 

158 

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

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

161 # table to use the short offsets. 

162 indices = [ 

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

164 ] 

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

166 # It fits. Do it. 

167 for i in indices: 

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

169 currentLocation = 0 

170 for i, glyphData in enumerate(dataList): 

171 locations[i] = currentLocation 

172 currentLocation += len(glyphData) 

173 locations[len(dataList)] = currentLocation 

174 

175 data = b"".join(dataList) 

176 if "loca" in ttFont: 

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

178 if "maxp" in ttFont: 

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

180 if not data: 

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

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

183 # on Windows as well. 

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

185 data = b"\0" 

186 return data 

187 

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

189 notice = ( 

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

191 "will be recalculated by the compiler." 

192 ) 

193 glyphNames = ttFont.getGlyphNames() 

194 if not splitGlyphs: 

195 writer.newline() 

196 writer.comment(notice) 

197 writer.newline() 

198 writer.newline() 

199 numGlyphs = len(glyphNames) 

200 if splitGlyphs: 

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

202 existingGlyphFiles = set() 

203 for glyphName in glyphNames: 

204 glyph = self.get(glyphName) 

205 if glyph is None: 

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

207 continue 

208 if glyph.numberOfContours: 

209 if splitGlyphs: 

210 glyphPath = userNameToFileName( 

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

212 existingGlyphFiles, 

213 prefix=path + ".", 

214 suffix=ext, 

215 ) 

216 existingGlyphFiles.add(glyphPath.lower()) 

217 glyphWriter = xmlWriter.XMLWriter( 

218 glyphPath, 

219 idlefunc=writer.idlefunc, 

220 newlinestr=writer.newlinestr, 

221 ) 

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

223 glyphWriter.newline() 

224 glyphWriter.begintag("glyf") 

225 glyphWriter.newline() 

226 glyphWriter.comment(notice) 

227 glyphWriter.newline() 

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

229 else: 

230 glyphWriter = writer 

231 glyphWriter.begintag( 

232 "TTGlyph", 

233 [ 

234 ("name", glyphName), 

235 ("xMin", glyph.xMin), 

236 ("yMin", glyph.yMin), 

237 ("xMax", glyph.xMax), 

238 ("yMax", glyph.yMax), 

239 ], 

240 ) 

241 glyphWriter.newline() 

242 glyph.toXML(glyphWriter, ttFont) 

243 glyphWriter.endtag("TTGlyph") 

244 glyphWriter.newline() 

245 if splitGlyphs: 

246 glyphWriter.endtag("glyf") 

247 glyphWriter.newline() 

248 glyphWriter.endtag("ttFont") 

249 glyphWriter.newline() 

250 glyphWriter.close() 

251 else: 

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

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

254 if not splitGlyphs: 

255 writer.newline() 

256 writer.newline() 

257 

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

259 if name != "TTGlyph": 

260 return 

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

262 self.glyphs = {} 

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

264 self.glyphOrder = ttFont.getGlyphOrder() 

265 glyphName = attrs["name"] 

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

267 glyph = Glyph() 

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

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

270 self.glyphs[glyphName] = glyph 

271 for element in content: 

272 if not isinstance(element, tuple): 

273 continue 

274 name, attrs, content = element 

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

276 if not ttFont.recalcBBoxes: 

277 glyph.compact(self, 0) 

278 

279 def setGlyphOrder(self, glyphOrder): 

280 """Sets the glyph order 

281 

282 Args: 

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

284 """ 

285 self.glyphOrder = glyphOrder 

286 self._reverseGlyphOrder = {} 

287 

288 def getGlyphName(self, glyphID): 

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

290 

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

292 """ 

293 return self.glyphOrder[glyphID] 

294 

295 def _buildReverseGlyphOrderDict(self): 

296 self._reverseGlyphOrder = d = {} 

297 for glyphID, glyphName in enumerate(self.glyphOrder): 

298 d[glyphName] = glyphID 

299 

300 def getGlyphID(self, glyphName): 

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

302 

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

304 """ 

305 glyphOrder = self.glyphOrder 

306 id = getattr(self, "_reverseGlyphOrder", {}).get(glyphName) 

307 if id is None or id >= len(glyphOrder) or glyphOrder[id] != glyphName: 

308 self._buildReverseGlyphOrderDict() 

309 id = self._reverseGlyphOrder.get(glyphName) 

310 if id is None: 

311 raise ValueError(glyphName) 

312 return id 

313 

314 def removeHinting(self): 

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

316 

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

318 """ 

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

320 glyph.removeHinting() 

321 

322 def keys(self): 

323 return self.glyphs.keys() 

324 

325 def has_key(self, glyphName): 

326 return glyphName in self.glyphs 

327 

328 __contains__ = has_key 

329 

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

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

332 if glyph is not None: 

333 glyph.expand(self) 

334 return glyph 

335 

336 def __getitem__(self, glyphName): 

337 glyph = self.glyphs[glyphName] 

338 glyph.expand(self) 

339 return glyph 

340 

341 def __setitem__(self, glyphName, glyph): 

342 self.glyphs[glyphName] = glyph 

343 if glyphName not in self.glyphOrder: 

344 self.glyphOrder.append(glyphName) 

345 

346 def __delitem__(self, glyphName): 

347 del self.glyphs[glyphName] 

348 self.glyphOrder.remove(glyphName) 

349 

350 def __len__(self): 

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

352 return len(self.glyphs) 

353 

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

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

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

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

358 

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

360 

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

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

363 

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

365 """ 

366 glyph = self[glyphName] 

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

368 glyph.recalcBounds(self) 

369 

370 horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName] 

371 leftSideX = glyph.xMin - leftSideBearing 

372 rightSideX = leftSideX + horizontalAdvanceWidth 

373 

374 if vMetrics: 

375 verticalAdvanceWidth, topSideBearing = vMetrics[glyphName] 

376 topSideY = topSideBearing + glyph.yMax 

377 bottomSideY = topSideY - verticalAdvanceWidth 

378 else: 

379 bottomSideY = topSideY = 0 

380 

381 return [ 

382 (leftSideX, 0), 

383 (rightSideX, 0), 

384 (0, topSideY), 

385 (0, bottomSideY), 

386 ] 

387 

388 def _getCoordinatesAndControls( 

389 self, glyphName, hMetrics, vMetrics=None, *, round=otRound 

390 ): 

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

392 

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

394 as mandated by the "gvar" spec. 

395 

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

397 - numberOfContours: -1 for composite glyphs. 

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

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

400 optimization). 

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

402 composite glyphs). 

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

404 composite glyphs (None for simple glyphs). 

405 

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

407 the "_getPhantomPoints" method). 

408 

409 Return None if the requested glyphName is not present. 

410 """ 

411 glyph = self.get(glyphName) 

412 if glyph is None: 

413 return None 

414 if glyph.isComposite(): 

415 coords = GlyphCoordinates( 

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

417 ) 

418 controls = _GlyphControls( 

419 numberOfContours=glyph.numberOfContours, 

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

421 flags=None, 

422 components=[ 

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

424 for c in glyph.components 

425 ], 

426 ) 

427 elif glyph.isVarComposite(): 

428 coords = [] 

429 controls = [] 

430 

431 for component in glyph.components: 

432 ( 

433 componentCoords, 

434 componentControls, 

435 ) = component.getCoordinatesAndControls() 

436 coords.extend(componentCoords) 

437 controls.extend(componentControls) 

438 

439 coords = GlyphCoordinates(coords) 

440 

441 controls = _GlyphControls( 

442 numberOfContours=glyph.numberOfContours, 

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

444 flags=None, 

445 components=[ 

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

447 ], 

448 ) 

449 

450 else: 

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

452 coords = coords.copy() 

453 controls = _GlyphControls( 

454 numberOfContours=glyph.numberOfContours, 

455 endPts=endPts, 

456 flags=flags, 

457 components=None, 

458 ) 

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

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

461 coords.extend(phantomPoints) 

462 coords.toInt(round=round) 

463 return coords, controls 

464 

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

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

467 

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

469 points" as the last four coordinates. 

470 

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

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

473 the glyph's bounding boxes. 

474 

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

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

477 method). 

478 """ 

479 glyph = self[glyphName] 

480 

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

482 assert len(coord) >= 4 

483 leftSideX = coord[-4][0] 

484 rightSideX = coord[-3][0] 

485 topSideY = coord[-2][1] 

486 bottomSideY = coord[-1][1] 

487 

488 coord = coord[:-4] 

489 

490 if glyph.isComposite(): 

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

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

493 if hasattr(comp, "x"): 

494 comp.x, comp.y = p 

495 elif glyph.isVarComposite(): 

496 for comp in glyph.components: 

497 coord = comp.setCoordinates(coord) 

498 assert not coord 

499 elif glyph.numberOfContours == 0: 

500 assert len(coord) == 0 

501 else: 

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

503 glyph.coordinates = GlyphCoordinates(coord) 

504 

505 glyph.recalcBounds(self, boundsDone=set()) 

506 

507 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 

508 if horizontalAdvanceWidth < 0: 

509 # unlikely, but it can happen, see: 

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

511 horizontalAdvanceWidth = 0 

512 leftSideBearing = otRound(glyph.xMin - leftSideX) 

513 hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing 

514 

515 if vMetrics is not None: 

516 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 

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

518 verticalAdvanceWidth = 0 

519 topSideBearing = otRound(topSideY - glyph.yMax) 

520 vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing 

521 

522 # Deprecated 

523 

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

525 """This method is wrong and deprecated. 

526 For rationale see: 

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

528 """ 

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

530 if vMetrics is None: 

531 verticalAdvanceWidth = ttFont["head"].unitsPerEm 

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

533 if topSideY is None: 

534 if defaultVerticalOrigin is not None: 

535 topSideY = defaultVerticalOrigin 

536 else: 

537 topSideY = verticalAdvanceWidth 

538 glyph = self[glyphName] 

539 glyph.recalcBounds(self) 

540 topSideBearing = otRound(topSideY - glyph.yMax) 

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

542 return vMetrics 

543 

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

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

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

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

548 hMetrics = ttFont["hmtx"].metrics 

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

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

551 

552 @deprecateFunction( 

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

554 ) 

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

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

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

558 hMetrics = ttFont["hmtx"].metrics 

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

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

561 

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

563 def setCoordinates(self, glyphName, ttFont): 

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

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

566 hMetrics = ttFont["hmtx"].metrics 

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

568 self._setCoordinates(glyphName, hMetrics, vMetrics) 

569 

570 

571_GlyphControls = namedtuple( 

572 "_GlyphControls", "numberOfContours endPts flags components" 

573) 

574 

575 

576glyphHeaderFormat = """ 

577 > # big endian 

578 numberOfContours: h 

579 xMin: h 

580 yMin: h 

581 xMax: h 

582 yMax: h 

583""" 

584 

585# flags 

586flagOnCurve = 0x01 

587flagXShort = 0x02 

588flagYShort = 0x04 

589flagRepeat = 0x08 

590flagXsame = 0x10 

591flagYsame = 0x20 

592flagOverlapSimple = 0x40 

593flagCubic = 0x80 

594 

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

596keepFlags = flagOnCurve + flagOverlapSimple + flagCubic 

597 

598_flagSignBytes = { 

599 0: 2, 

600 flagXsame: 0, 

601 flagXShort | flagXsame: +1, 

602 flagXShort: -1, 

603 flagYsame: 0, 

604 flagYShort | flagYsame: +1, 

605 flagYShort: -1, 

606} 

607 

608 

609def flagBest(x, y, onCurve): 

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

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

612 

613 flag = flagOnCurve if onCurve else 0 

614 cost = 0 

615 # do x 

616 if x == 0: 

617 flag = flag | flagXsame 

618 elif -255 <= x <= 255: 

619 flag = flag | flagXShort 

620 if x > 0: 

621 flag = flag | flagXsame 

622 cost += 1 

623 else: 

624 cost += 2 

625 # do y 

626 if y == 0: 

627 flag = flag | flagYsame 

628 elif -255 <= y <= 255: 

629 flag = flag | flagYShort 

630 if y > 0: 

631 flag = flag | flagYsame 

632 cost += 1 

633 else: 

634 cost += 2 

635 return flag, cost 

636 

637 

638def flagFits(newFlag, oldFlag, mask): 

639 newBytes = _flagSignBytes[newFlag & mask] 

640 oldBytes = _flagSignBytes[oldFlag & mask] 

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

642 

643 

644def flagSupports(newFlag, oldFlag): 

645 return ( 

646 (oldFlag & flagOnCurve) == (newFlag & flagOnCurve) 

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

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

649 ) 

650 

651 

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

653 byteCount = _flagSignBytes[flag & mask] 

654 if byteCount == 1: 

655 coordBytes.append(coord) 

656 elif byteCount == -1: 

657 coordBytes.append(-coord) 

658 elif byteCount == 2: 

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

660 

661 

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

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

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

665 

666 

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

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

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

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

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

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

673WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy 

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

675WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow 

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

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

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

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

680 

681 

682CompositeMaxpValues = namedtuple( 

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

684) 

685 

686 

687class Glyph(object): 

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

689 

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

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

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

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

694 attributes. 

695 

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

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

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

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

700 access its data. 

701 

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

703 

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

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

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

707 

708 """ 

709 

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

711 if not data: 

712 # empty char 

713 self.numberOfContours = 0 

714 return 

715 self.data = data 

716 

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

718 data = self.compile(glyfTable, recalcBBoxes) 

719 self.__dict__.clear() 

720 self.data = data 

721 

722 def expand(self, glyfTable): 

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

724 # already unpacked 

725 return 

726 if not self.data: 

727 # empty char 

728 del self.data 

729 self.numberOfContours = 0 

730 return 

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

732 del self.data 

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

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

735 # one, so short-circuit here. 

736 if self.numberOfContours == 0: 

737 return 

738 if self.isComposite(): 

739 self.decompileComponents(data, glyfTable) 

740 elif self.isVarComposite(): 

741 self.decompileVarComponents(data, glyfTable) 

742 else: 

743 self.decompileCoordinates(data) 

744 

745 def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): 

746 if hasattr(self, "data"): 

747 if recalcBBoxes: 

748 # must unpack glyph in order to recalculate bounding box 

749 self.expand(glyfTable) 

750 else: 

751 return self.data 

752 if self.numberOfContours == 0: 

753 return b"" 

754 

755 if recalcBBoxes: 

756 self.recalcBounds(glyfTable, boundsDone=boundsDone) 

757 

758 data = sstruct.pack(glyphHeaderFormat, self) 

759 if self.isComposite(): 

760 data = data + self.compileComponents(glyfTable) 

761 elif self.isVarComposite(): 

762 data = data + self.compileVarComponents(glyfTable) 

763 else: 

764 data = data + self.compileCoordinates() 

765 return data 

766 

767 def toXML(self, writer, ttFont): 

768 if self.isComposite(): 

769 for compo in self.components: 

770 compo.toXML(writer, ttFont) 

771 haveInstructions = hasattr(self, "program") 

772 elif self.isVarComposite(): 

773 for compo in self.components: 

774 compo.toXML(writer, ttFont) 

775 haveInstructions = False 

776 else: 

777 last = 0 

778 for i in range(self.numberOfContours): 

779 writer.begintag("contour") 

780 writer.newline() 

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

782 attrs = [ 

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

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

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

786 ] 

787 if self.flags[j] & flagOverlapSimple: 

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

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

790 if self.flags[j] & flagCubic: 

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

792 writer.simpletag("pt", attrs) 

793 writer.newline() 

794 last = self.endPtsOfContours[i] + 1 

795 writer.endtag("contour") 

796 writer.newline() 

797 haveInstructions = self.numberOfContours > 0 

798 if haveInstructions: 

799 if self.program: 

800 writer.begintag("instructions") 

801 writer.newline() 

802 self.program.toXML(writer, ttFont) 

803 writer.endtag("instructions") 

804 else: 

805 writer.simpletag("instructions") 

806 writer.newline() 

807 

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

809 if name == "contour": 

810 if self.numberOfContours < 0: 

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

812 self.numberOfContours = self.numberOfContours + 1 

813 coordinates = GlyphCoordinates() 

814 flags = bytearray() 

815 for element in content: 

816 if not isinstance(element, tuple): 

817 continue 

818 name, attrs, content = element 

819 if name != "pt": 

820 continue # ignore anything but "pt" 

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

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

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

824 flag |= flagOverlapSimple 

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

826 flag |= flagCubic 

827 flags.append(flag) 

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

829 self.coordinates = coordinates 

830 self.flags = flags 

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

832 else: 

833 self.coordinates.extend(coordinates) 

834 self.flags.extend(flags) 

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

836 elif name == "component": 

837 if self.numberOfContours > 0: 

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

839 self.numberOfContours = -1 

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

841 self.components = [] 

842 component = GlyphComponent() 

843 self.components.append(component) 

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

845 elif name == "varComponent": 

846 if self.numberOfContours > 0: 

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

848 self.numberOfContours = -2 

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

850 self.components = [] 

851 component = GlyphVarComponent() 

852 self.components.append(component) 

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

854 elif name == "instructions": 

855 self.program = ttProgram.Program() 

856 for element in content: 

857 if not isinstance(element, tuple): 

858 continue 

859 name, attrs, content = element 

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

861 

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

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

864 nContours = 0 

865 nPoints = 0 

866 initialMaxComponentDepth = maxComponentDepth 

867 for compo in self.components: 

868 baseGlyph = glyfTable[compo.glyphName] 

869 if baseGlyph.numberOfContours == 0: 

870 continue 

871 elif baseGlyph.numberOfContours > 0: 

872 nP, nC = baseGlyph.getMaxpValues() 

873 else: 

874 nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues( 

875 glyfTable, initialMaxComponentDepth + 1 

876 ) 

877 maxComponentDepth = max(maxComponentDepth, componentDepth) 

878 nPoints = nPoints + nP 

879 nContours = nContours + nC 

880 return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) 

881 

882 def getMaxpValues(self): 

883 assert self.numberOfContours > 0 

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

885 

886 def decompileComponents(self, data, glyfTable): 

887 self.components = [] 

888 more = 1 

889 haveInstructions = 0 

890 while more: 

891 component = GlyphComponent() 

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

893 haveInstructions = haveInstructions | haveInstr 

894 self.components.append(component) 

895 if haveInstructions: 

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

897 data = data[2:] 

898 self.program = ttProgram.Program() 

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

900 data = data[numInstructions:] 

901 if len(data) >= 4: 

902 log.warning( 

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

904 len(data), 

905 ) 

906 

907 def decompileVarComponents(self, data, glyfTable): 

908 self.components = [] 

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

910 component = GlyphVarComponent() 

911 data = component.decompile(data, glyfTable) 

912 self.components.append(component) 

913 

914 def decompileCoordinates(self, data): 

915 endPtsOfContours = array.array("H") 

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

917 if sys.byteorder != "big": 

918 endPtsOfContours.byteswap() 

919 self.endPtsOfContours = endPtsOfContours.tolist() 

920 

921 pos = 2 * self.numberOfContours 

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

923 self.program = ttProgram.Program() 

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

925 pos += 2 + instructionLength 

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

927 flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw( 

928 nCoordinates, data, pos 

929 ) 

930 

931 # fill in repetitions and apply signs 

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

933 xIndex = 0 

934 yIndex = 0 

935 for i in range(nCoordinates): 

936 flag = flags[i] 

937 # x coordinate 

938 if flag & flagXShort: 

939 if flag & flagXsame: 

940 x = xCoordinates[xIndex] 

941 else: 

942 x = -xCoordinates[xIndex] 

943 xIndex = xIndex + 1 

944 elif flag & flagXsame: 

945 x = 0 

946 else: 

947 x = xCoordinates[xIndex] 

948 xIndex = xIndex + 1 

949 # y coordinate 

950 if flag & flagYShort: 

951 if flag & flagYsame: 

952 y = yCoordinates[yIndex] 

953 else: 

954 y = -yCoordinates[yIndex] 

955 yIndex = yIndex + 1 

956 elif flag & flagYsame: 

957 y = 0 

958 else: 

959 y = yCoordinates[yIndex] 

960 yIndex = yIndex + 1 

961 coordinates[i] = (x, y) 

962 assert xIndex == len(xCoordinates) 

963 assert yIndex == len(yCoordinates) 

964 coordinates.relativeToAbsolute() 

965 # discard all flags except "keepFlags" 

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

967 flags[i] &= keepFlags 

968 self.flags = flags 

969 

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

971 # unpack flags and prepare unpacking of coordinates 

972 flags = bytearray(nCoordinates) 

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

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

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

976 xFormat = ">" # big endian 

977 yFormat = ">" # big endian 

978 j = 0 

979 while True: 

980 flag = data[pos] 

981 pos += 1 

982 repeat = 1 

983 if flag & flagRepeat: 

984 repeat = data[pos] + 1 

985 pos += 1 

986 for k in range(repeat): 

987 if flag & flagXShort: 

988 xFormat = xFormat + "B" 

989 elif not (flag & flagXsame): 

990 xFormat = xFormat + "h" 

991 if flag & flagYShort: 

992 yFormat = yFormat + "B" 

993 elif not (flag & flagYsame): 

994 yFormat = yFormat + "h" 

995 flags[j] = flag 

996 j = j + 1 

997 if j >= nCoordinates: 

998 break 

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

1000 # unpack raw coordinates, krrrrrr-tching! 

1001 xDataLen = struct.calcsize(xFormat) 

1002 yDataLen = struct.calcsize(yFormat) 

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

1004 log.warning( 

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

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

1007 ) 

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

1009 yCoordinates = struct.unpack( 

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

1011 ) 

1012 return flags, xCoordinates, yCoordinates 

1013 

1014 def compileComponents(self, glyfTable): 

1015 data = b"" 

1016 lastcomponent = len(self.components) - 1 

1017 more = 1 

1018 haveInstructions = 0 

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

1020 if i == lastcomponent: 

1021 haveInstructions = hasattr(self, "program") 

1022 more = 0 

1023 compo = self.components[i] 

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

1025 if haveInstructions: 

1026 instructions = self.program.getBytecode() 

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

1028 return data 

1029 

1030 def compileVarComponents(self, glyfTable): 

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

1032 

1033 def compileCoordinates(self): 

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

1035 data = [] 

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

1037 if sys.byteorder != "big": 

1038 endPtsOfContours.byteswap() 

1039 data.append(endPtsOfContours.tobytes()) 

1040 instructions = self.program.getBytecode() 

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

1042 data.append(instructions) 

1043 

1044 deltas = self.coordinates.copy() 

1045 deltas.toInt() 

1046 deltas.absoluteToRelative() 

1047 

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

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

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

1051 

1052 data.extend(deltas) 

1053 return b"".join(data) 

1054 

1055 def compileDeltasGreedy(self, flags, deltas): 

1056 # Implements greedy algorithm for packing coordinate deltas: 

1057 # uses shortest representation one coordinate at a time. 

1058 compressedFlags = bytearray() 

1059 compressedXs = bytearray() 

1060 compressedYs = bytearray() 

1061 lastflag = None 

1062 repeat = 0 

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

1064 # Oh, the horrors of TrueType 

1065 # do x 

1066 if x == 0: 

1067 flag = flag | flagXsame 

1068 elif -255 <= x <= 255: 

1069 flag = flag | flagXShort 

1070 if x > 0: 

1071 flag = flag | flagXsame 

1072 else: 

1073 x = -x 

1074 compressedXs.append(x) 

1075 else: 

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

1077 # do y 

1078 if y == 0: 

1079 flag = flag | flagYsame 

1080 elif -255 <= y <= 255: 

1081 flag = flag | flagYShort 

1082 if y > 0: 

1083 flag = flag | flagYsame 

1084 else: 

1085 y = -y 

1086 compressedYs.append(y) 

1087 else: 

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

1089 # handle repeating flags 

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

1091 repeat = repeat + 1 

1092 if repeat == 1: 

1093 compressedFlags.append(flag) 

1094 else: 

1095 compressedFlags[-2] = flag | flagRepeat 

1096 compressedFlags[-1] = repeat 

1097 else: 

1098 repeat = 0 

1099 compressedFlags.append(flag) 

1100 lastflag = flag 

1101 return (compressedFlags, compressedXs, compressedYs) 

1102 

1103 def compileDeltasOptimal(self, flags, deltas): 

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

1105 # deltas. The savings are negligible :(. 

1106 candidates = [] 

1107 bestTuple = None 

1108 bestCost = 0 

1109 repeat = 0 

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

1111 # Oh, the horrors of TrueType 

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

1113 bestCost += 1 + coordBytes 

1114 newCandidates = [ 

1115 (bestCost, bestTuple, flag, coordBytes), 

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

1117 ] 

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

1119 if ( 

1120 lastCost + coordBytes <= bestCost + 1 

1121 and (lastFlag & flagRepeat) 

1122 and (lastFlag < 0xFF00) 

1123 and flagSupports(lastFlag, flag) 

1124 ): 

1125 if (lastFlag & 0xFF) == ( 

1126 flag | flagRepeat 

1127 ) and lastCost == bestCost + 1: 

1128 continue 

1129 newCandidates.append( 

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

1131 ) 

1132 candidates = newCandidates 

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

1134 bestCost = bestTuple[0] 

1135 

1136 flags = [] 

1137 while bestTuple: 

1138 cost, bestTuple, flag, coordBytes = bestTuple 

1139 flags.append(flag) 

1140 flags.reverse() 

1141 

1142 compressedFlags = bytearray() 

1143 compressedXs = bytearray() 

1144 compressedYs = bytearray() 

1145 coords = iter(deltas) 

1146 ff = [] 

1147 for flag in flags: 

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

1149 compressedFlags.append(flag) 

1150 if flag & flagRepeat: 

1151 assert repeatCount > 0 

1152 compressedFlags.append(repeatCount) 

1153 else: 

1154 assert repeatCount == 0 

1155 for i in range(1 + repeatCount): 

1156 x, y = next(coords) 

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

1158 ff.append(flag) 

1159 try: 

1160 next(coords) 

1161 raise Exception("internal error") 

1162 except StopIteration: 

1163 pass 

1164 

1165 return (compressedFlags, compressedXs, compressedYs) 

1166 

1167 def recalcBounds(self, glyfTable, *, boundsDone=None): 

1168 """Recalculates the bounds of the glyph. 

1169 

1170 Each glyph object stores its bounding box in the 

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

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

1173 must be provided to resolve component bounds. 

1174 """ 

1175 if self.isComposite() and self.tryRecalcBoundsComposite( 

1176 glyfTable, boundsDone=boundsDone 

1177 ): 

1178 return 

1179 try: 

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

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

1182 except NotImplementedError: 

1183 pass 

1184 

1185 def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None): 

1186 """Try recalculating the bounds of a composite glyph that has 

1187 certain constrained properties. Namely, none of the components 

1188 have a transform other than an integer translate, and none 

1189 uses the anchor points. 

1190 

1191 Each glyph object stores its bounding box in the 

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

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

1194 must be provided to resolve component bounds. 

1195 

1196 Return True if bounds were calculated, False otherwise. 

1197 """ 

1198 for compo in self.components: 

1199 if hasattr(compo, "firstPt") or hasattr(compo, "transform"): 

1200 return False 

1201 if not float(compo.x).is_integer() or not float(compo.y).is_integer(): 

1202 return False 

1203 

1204 # All components are untransformed and have an integer x/y translate 

1205 bounds = None 

1206 for compo in self.components: 

1207 glyphName = compo.glyphName 

1208 g = glyfTable[glyphName] 

1209 

1210 if boundsDone is None or glyphName not in boundsDone: 

1211 g.recalcBounds(glyfTable, boundsDone=boundsDone) 

1212 if boundsDone is not None: 

1213 boundsDone.add(glyphName) 

1214 # empty components shouldn't update the bounds of the parent glyph 

1215 if g.numberOfContours == 0: 

1216 continue 

1217 

1218 x, y = compo.x, compo.y 

1219 bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y)) 

1220 bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y)) 

1221 

1222 if bounds is None: 

1223 bounds = (0, 0, 0, 0) 

1224 self.xMin, self.yMin, self.xMax, self.yMax = bounds 

1225 return True 

1226 

1227 def isComposite(self): 

1228 """Test whether a glyph has components""" 

1229 if hasattr(self, "data"): 

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

1231 else: 

1232 return self.numberOfContours == -1 

1233 

1234 def isVarComposite(self): 

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

1236 if hasattr(self, "data"): 

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

1238 else: 

1239 return self.numberOfContours == -2 

1240 

1241 def getCoordinates(self, glyfTable): 

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

1243 

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

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

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

1247 

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

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

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

1251 in the glyph. 

1252 

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

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

1255 """ 

1256 

1257 if self.numberOfContours > 0: 

1258 return self.coordinates, self.endPtsOfContours, self.flags 

1259 elif self.isComposite(): 

1260 # it's a composite 

1261 allCoords = GlyphCoordinates() 

1262 allFlags = bytearray() 

1263 allEndPts = [] 

1264 for compo in self.components: 

1265 g = glyfTable[compo.glyphName] 

1266 try: 

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

1268 except RecursionError: 

1269 raise ttLib.TTLibError( 

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

1271 % compo.glyphName 

1272 ) 

1273 coordinates = GlyphCoordinates(coordinates) 

1274 if hasattr(compo, "firstPt"): 

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

1276 # computing the offset between the points 

1277 if hasattr(compo, "transform"): 

1278 coordinates.transform(compo.transform) 

1279 x1, y1 = allCoords[compo.firstPt] 

1280 x2, y2 = coordinates[compo.secondPt] 

1281 move = x1 - x2, y1 - y2 

1282 coordinates.translate(move) 

1283 else: 

1284 # component uses XY offsets 

1285 move = compo.x, compo.y 

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

1287 coordinates.translate(move) 

1288 else: 

1289 apple_way = compo.flags & SCALED_COMPONENT_OFFSET 

1290 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET 

1291 assert not (apple_way and ms_way) 

1292 if not (apple_way or ms_way): 

1293 scale_component_offset = ( 

1294 SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file 

1295 ) 

1296 else: 

1297 scale_component_offset = apple_way 

1298 if scale_component_offset: 

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

1300 coordinates.translate(move) 

1301 coordinates.transform(compo.transform) 

1302 else: 

1303 # the MS way: first scale, then move 

1304 coordinates.transform(compo.transform) 

1305 coordinates.translate(move) 

1306 offset = len(allCoords) 

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

1308 allCoords.extend(coordinates) 

1309 allFlags.extend(flags) 

1310 return allCoords, allEndPts, allFlags 

1311 elif self.isVarComposite(): 

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

1313 else: 

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

1315 

1316 def getComponentNames(self, glyfTable): 

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

1318 

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

1320 empty list) or composite glyphs. 

1321 """ 

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

1323 # TODO(VarComposite) Add implementation without expanding glyph 

1324 self.expand(glyfTable) 

1325 

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

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

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

1329 else: 

1330 return [] 

1331 

1332 # Extract components without expanding glyph 

1333 

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

1335 return [] # Not composite 

1336 

1337 data = self.data 

1338 i = 10 

1339 components = [] 

1340 more = 1 

1341 while more: 

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

1343 i += 4 

1344 flags = int(flags) 

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

1346 

1347 if flags & ARG_1_AND_2_ARE_WORDS: 

1348 i += 4 

1349 else: 

1350 i += 2 

1351 if flags & WE_HAVE_A_SCALE: 

1352 i += 2 

1353 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1354 i += 4 

1355 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1356 i += 8 

1357 more = flags & MORE_COMPONENTS 

1358 

1359 return components 

1360 

1361 def trim(self, remove_hinting=False): 

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

1363 This works on both expanded and compacted glyphs, without 

1364 expanding it.""" 

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

1366 if remove_hinting: 

1367 if self.isComposite(): 

1368 if hasattr(self, "program"): 

1369 del self.program 

1370 elif self.isVarComposite(): 

1371 pass # Doesn't have hinting 

1372 else: 

1373 self.program = ttProgram.Program() 

1374 self.program.fromBytecode([]) 

1375 # No padding to trim. 

1376 return 

1377 if not self.data: 

1378 return 

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

1380 data = bytearray(self.data) 

1381 i = 10 

1382 if numContours >= 0: 

1383 i += 2 * numContours # endPtsOfContours 

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

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

1386 if remove_hinting: 

1387 # Zero instruction length 

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

1389 i += 2 

1390 if instructionLen: 

1391 # Splice it out 

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

1393 instructionLen = 0 

1394 else: 

1395 i += 2 + instructionLen 

1396 

1397 coordBytes = 0 

1398 j = 0 

1399 while True: 

1400 flag = data[i] 

1401 i = i + 1 

1402 repeat = 1 

1403 if flag & flagRepeat: 

1404 repeat = data[i] + 1 

1405 i = i + 1 

1406 xBytes = yBytes = 0 

1407 if flag & flagXShort: 

1408 xBytes = 1 

1409 elif not (flag & flagXsame): 

1410 xBytes = 2 

1411 if flag & flagYShort: 

1412 yBytes = 1 

1413 elif not (flag & flagYsame): 

1414 yBytes = 2 

1415 coordBytes += (xBytes + yBytes) * repeat 

1416 j += repeat 

1417 if j >= nCoordinates: 

1418 break 

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

1420 i += coordBytes 

1421 # Remove padding 

1422 data = data[:i] 

1423 elif self.isComposite(): 

1424 more = 1 

1425 we_have_instructions = False 

1426 while more: 

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

1428 if remove_hinting: 

1429 flags &= ~WE_HAVE_INSTRUCTIONS 

1430 if flags & WE_HAVE_INSTRUCTIONS: 

1431 we_have_instructions = True 

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

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

1434 i += 4 

1435 flags = int(flags) 

1436 

1437 if flags & ARG_1_AND_2_ARE_WORDS: 

1438 i += 4 

1439 else: 

1440 i += 2 

1441 if flags & WE_HAVE_A_SCALE: 

1442 i += 2 

1443 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1444 i += 4 

1445 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1446 i += 8 

1447 more = flags & MORE_COMPONENTS 

1448 if we_have_instructions: 

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

1450 i += 2 + instructionLen 

1451 # Remove padding 

1452 data = data[:i] 

1453 elif self.isVarComposite(): 

1454 i = 0 

1455 MIN_SIZE = GlyphVarComponent.MIN_SIZE 

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

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

1458 i += size 

1459 data = data[:i] 

1460 

1461 self.data = data 

1462 

1463 def removeHinting(self): 

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

1465 self.trim(remove_hinting=True) 

1466 

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

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

1469 

1470 Arguments: 

1471 pen: An object conforming to the pen protocol. 

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

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

1474 translated by this offset. 

1475 """ 

1476 

1477 if self.isComposite(): 

1478 for component in self.components: 

1479 glyphName, transform = component.getComponentInfo() 

1480 pen.addComponent(glyphName, transform) 

1481 return 

1482 

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

1484 if offset: 

1485 coordinates = coordinates.copy() 

1486 coordinates.translate((offset, 0)) 

1487 start = 0 

1488 maybeInt = lambda v: int(v) if v == int(v) else v 

1489 for end in endPts: 

1490 end = end + 1 

1491 contour = coordinates[start:end] 

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

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

1494 start = end 

1495 if 1 not in cFlags: 

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

1497 cubic = all(cuFlags) 

1498 if cubic: 

1499 count = len(contour) 

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

1501 l = contour[-1] 

1502 f = contour[0] 

1503 p0 = (maybeInt((l[0] + f[0]) * 0.5), maybeInt((l[1] + f[1]) * 0.5)) 

1504 pen.moveTo(p0) 

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

1506 p1 = contour[i] 

1507 p2 = contour[i + 1] 

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

1509 p3 = ( 

1510 maybeInt((p2[0] + p4[0]) * 0.5), 

1511 maybeInt((p2[1] + p4[1]) * 0.5), 

1512 ) 

1513 pen.curveTo(p1, p2, p3) 

1514 else: 

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

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

1517 # as the on-curve point. 

1518 contour.append(None) 

1519 pen.qCurveTo(*contour) 

1520 else: 

1521 # Shuffle the points so that the contour is guaranteed 

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

1523 # the moveTo. 

1524 firstOnCurve = cFlags.index(1) + 1 

1525 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 

1526 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 

1527 cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve] 

1528 pen.moveTo(contour[-1]) 

1529 while contour: 

1530 nextOnCurve = cFlags.index(1) + 1 

1531 if nextOnCurve == 1: 

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

1533 # pen.closePath() 

1534 if len(contour) > 1: 

1535 pen.lineTo(contour[0]) 

1536 else: 

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

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

1539 cubic = any(cubicFlags) 

1540 if cubic: 

1541 assert all( 

1542 cubicFlags 

1543 ), "Mixed cubic and quadratic segment undefined" 

1544 

1545 count = nextOnCurve 

1546 assert ( 

1547 count >= 3 

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

1549 assert ( 

1550 count - 1 

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

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

1553 p1 = contour[i] 

1554 p2 = contour[i + 1] 

1555 p4 = contour[i + 2] 

1556 p3 = ( 

1557 maybeInt((p2[0] + p4[0]) * 0.5), 

1558 maybeInt((p2[1] + p4[1]) * 0.5), 

1559 ) 

1560 lastOnCurve = p3 

1561 pen.curveTo(p1, p2, p3) 

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

1563 else: 

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

1565 contour = contour[nextOnCurve:] 

1566 cFlags = cFlags[nextOnCurve:] 

1567 cuFlags = cuFlags[nextOnCurve:] 

1568 pen.closePath() 

1569 

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

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

1572 this will not change the point indices. 

1573 """ 

1574 

1575 if self.isComposite(): 

1576 for component in self.components: 

1577 glyphName, transform = component.getComponentInfo() 

1578 pen.addComponent(glyphName, transform) 

1579 return 

1580 

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

1582 if offset: 

1583 coordinates = coordinates.copy() 

1584 coordinates.translate((offset, 0)) 

1585 start = 0 

1586 for end in endPts: 

1587 end = end + 1 

1588 contour = coordinates[start:end] 

1589 cFlags = flags[start:end] 

1590 start = end 

1591 pen.beginPath() 

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

1593 

1594 if cFlags[-1] & flagOnCurve: 

1595 segmentType = "line" 

1596 elif cFlags[-1] & flagCubic: 

1597 segmentType = "curve" 

1598 else: 

1599 segmentType = "qcurve" 

1600 for i, pt in enumerate(contour): 

1601 if cFlags[i] & flagOnCurve: 

1602 pen.addPoint(pt, segmentType=segmentType) 

1603 segmentType = "line" 

1604 else: 

1605 pen.addPoint(pt) 

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

1607 pen.endPath() 

1608 

1609 def __eq__(self, other): 

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

1611 return NotImplemented 

1612 return self.__dict__ == other.__dict__ 

1613 

1614 def __ne__(self, other): 

1615 result = self.__eq__(other) 

1616 return result if result is NotImplemented else not result 

1617 

1618 

1619# Vector.__round__ uses the built-in (Banker's) `round` but we want 

1620# to use otRound below 

1621_roundv = partial(Vector.__round__, round=otRound) 

1622 

1623 

1624def _is_mid_point(p0: tuple, p1: tuple, p2: tuple) -> bool: 

1625 # True if p1 is in the middle of p0 and p2, either before or after rounding 

1626 p0 = Vector(p0) 

1627 p1 = Vector(p1) 

1628 p2 = Vector(p2) 

1629 return ((p0 + p2) * 0.5).isclose(p1) or _roundv(p0) + _roundv(p2) == _roundv(p1) * 2 

1630 

1631 

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

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

1634 

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

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

1637 

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

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

1640 for all of them will actually be implied. 

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

1642 contours are considered. 

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

1644 

1645 Args: 

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

1647 

1648 Returns: 

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

1650 

1651 Raises: 

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

1653 different point flags or number of contours. 

1654 

1655 Reference: 

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

1657 """ 

1658 staticAttributes = SimpleNamespace( 

1659 numberOfContours=None, flags=None, endPtsOfContours=None 

1660 ) 

1661 drop = None 

1662 simple_glyphs = [] 

1663 for i, glyph in enumerate(interpolatable_glyphs): 

1664 if glyph.numberOfContours < 1: 

1665 # ignore composite or empty glyphs 

1666 continue 

1667 

1668 for attr in staticAttributes.__dict__: 

1669 expected = getattr(staticAttributes, attr) 

1670 found = getattr(glyph, attr) 

1671 if expected is None: 

1672 setattr(staticAttributes, attr, found) 

1673 elif expected != found: 

1674 raise ValueError( 

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

1676 f"expected {expected}, found {found}" 

1677 ) 

1678 

1679 may_drop = set() 

1680 start = 0 

1681 coords = glyph.coordinates 

1682 flags = staticAttributes.flags 

1683 endPtsOfContours = staticAttributes.endPtsOfContours 

1684 for last in endPtsOfContours: 

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

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

1687 continue 

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

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

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

1691 continue 

1692 # we may drop the ith on-curve if halfway between previous/next off-curves 

1693 if not _is_mid_point(coords[prv], coords[i], coords[nxt]): 

1694 continue 

1695 

1696 may_drop.add(i) 

1697 start = last + 1 

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

1699 if drop is None: 

1700 drop = may_drop 

1701 else: 

1702 drop.intersection_update(may_drop) 

1703 

1704 simple_glyphs.append(glyph) 

1705 

1706 if drop: 

1707 # Do the actual dropping 

1708 flags = staticAttributes.flags 

1709 assert flags is not None 

1710 newFlags = array.array( 

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

1712 ) 

1713 

1714 endPts = staticAttributes.endPtsOfContours 

1715 assert endPts is not None 

1716 newEndPts = [] 

1717 i = 0 

1718 delta = 0 

1719 for d in sorted(drop): 

1720 while d > endPts[i]: 

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

1722 i += 1 

1723 delta += 1 

1724 while i < len(endPts): 

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

1726 i += 1 

1727 

1728 for glyph in simple_glyphs: 

1729 coords = glyph.coordinates 

1730 glyph.coordinates = GlyphCoordinates( 

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

1732 ) 

1733 glyph.flags = newFlags 

1734 glyph.endPtsOfContours = newEndPts 

1735 

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

1737 

1738 

1739class GlyphComponent(object): 

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

1741 

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

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

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

1745 attribute is not present. 

1746 """ 

1747 

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

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

1750 # mind - see below. 

1751 

1752 def __init__(self): 

1753 pass 

1754 

1755 def getComponentInfo(self): 

1756 """Return information about the component 

1757 

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

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

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

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

1762 matrix is not present. 

1763 """ 

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

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

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

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

1768 # this TT feature. 

1769 if hasattr(self, "transform"): 

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

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

1772 else: 

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

1774 return self.glyphName, trans 

1775 

1776 def decompile(self, data, glyfTable): 

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

1778 self.flags = int(flags) 

1779 glyphID = int(glyphID) 

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

1781 data = data[4:] 

1782 

1783 if self.flags & ARG_1_AND_2_ARE_WORDS: 

1784 if self.flags & ARGS_ARE_XY_VALUES: 

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

1786 else: 

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

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

1789 data = data[4:] 

1790 else: 

1791 if self.flags & ARGS_ARE_XY_VALUES: 

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

1793 else: 

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

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

1796 data = data[2:] 

1797 

1798 if self.flags & WE_HAVE_A_SCALE: 

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

1800 self.transform = [ 

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

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

1803 ] # fixed 2.14 

1804 data = data[2:] 

1805 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: 

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

1807 self.transform = [ 

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

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

1810 ] # fixed 2.14 

1811 data = data[4:] 

1812 elif self.flags & WE_HAVE_A_TWO_BY_TWO: 

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

1814 self.transform = [ 

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

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

1817 ] # fixed 2.14 

1818 data = data[8:] 

1819 more = self.flags & MORE_COMPONENTS 

1820 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS 

1821 self.flags = self.flags & ( 

1822 ROUND_XY_TO_GRID 

1823 | USE_MY_METRICS 

1824 | SCALED_COMPONENT_OFFSET 

1825 | UNSCALED_COMPONENT_OFFSET 

1826 | NON_OVERLAPPING 

1827 | OVERLAP_COMPOUND 

1828 ) 

1829 return more, haveInstructions, data 

1830 

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

1832 data = b"" 

1833 

1834 # reset all flags we will calculate ourselves 

1835 flags = self.flags & ( 

1836 ROUND_XY_TO_GRID 

1837 | USE_MY_METRICS 

1838 | SCALED_COMPONENT_OFFSET 

1839 | UNSCALED_COMPONENT_OFFSET 

1840 | NON_OVERLAPPING 

1841 | OVERLAP_COMPOUND 

1842 ) 

1843 if more: 

1844 flags = flags | MORE_COMPONENTS 

1845 if haveInstructions: 

1846 flags = flags | WE_HAVE_INSTRUCTIONS 

1847 

1848 if hasattr(self, "firstPt"): 

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

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

1851 else: 

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

1853 flags = flags | ARG_1_AND_2_ARE_WORDS 

1854 else: 

1855 x = otRound(self.x) 

1856 y = otRound(self.y) 

1857 flags = flags | ARGS_ARE_XY_VALUES 

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

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

1860 else: 

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

1862 flags = flags | ARG_1_AND_2_ARE_WORDS 

1863 

1864 if hasattr(self, "transform"): 

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

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

1867 flags = flags | WE_HAVE_A_TWO_BY_TWO 

1868 data = data + struct.pack( 

1869 ">hhhh", 

1870 transform[0][0], 

1871 transform[0][1], 

1872 transform[1][0], 

1873 transform[1][1], 

1874 ) 

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

1876 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE 

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

1878 else: 

1879 flags = flags | WE_HAVE_A_SCALE 

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

1881 

1882 glyphID = glyfTable.getGlyphID(self.glyphName) 

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

1884 

1885 def toXML(self, writer, ttFont): 

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

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

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

1889 else: 

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

1891 

1892 if hasattr(self, "transform"): 

1893 transform = self.transform 

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

1895 attrs = attrs + [ 

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

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

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

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

1900 ] 

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

1902 attrs = attrs + [ 

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

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

1905 ] 

1906 else: 

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

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

1909 writer.simpletag("component", attrs) 

1910 writer.newline() 

1911 

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

1913 self.glyphName = attrs["glyphName"] 

1914 if "firstPt" in attrs: 

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

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

1917 else: 

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

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

1920 if "scale01" in attrs: 

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

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

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

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

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

1926 elif "scalex" in attrs: 

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

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

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

1930 elif "scale" in attrs: 

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

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

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

1934 

1935 def __eq__(self, other): 

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

1937 return NotImplemented 

1938 return self.__dict__ == other.__dict__ 

1939 

1940 def __ne__(self, other): 

1941 result = self.__eq__(other) 

1942 return result if result is NotImplemented else not result 

1943 

1944 

1945# 

1946# Variable Composite glyphs 

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

1948# 

1949 

1950 

1951class VarComponentFlags(IntFlag): 

1952 USE_MY_METRICS = 0x0001 

1953 AXIS_INDICES_ARE_SHORT = 0x0002 

1954 UNIFORM_SCALE = 0x0004 

1955 HAVE_TRANSLATE_X = 0x0008 

1956 HAVE_TRANSLATE_Y = 0x0010 

1957 HAVE_ROTATION = 0x0020 

1958 HAVE_SCALE_X = 0x0040 

1959 HAVE_SCALE_Y = 0x0080 

1960 HAVE_SKEW_X = 0x0100 

1961 HAVE_SKEW_Y = 0x0200 

1962 HAVE_TCENTER_X = 0x0400 

1963 HAVE_TCENTER_Y = 0x0800 

1964 GID_IS_24BIT = 0x1000 

1965 AXES_HAVE_VARIATION = 0x2000 

1966 RESET_UNSPECIFIED_AXES = 0x4000 

1967 

1968 

1969VarComponentTransformMappingValues = namedtuple( 

1970 "VarComponentTransformMappingValues", 

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

1972) 

1973 

1974VAR_COMPONENT_TRANSFORM_MAPPING = { 

1975 "translateX": VarComponentTransformMappingValues( 

1976 VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 

1977 ), 

1978 "translateY": VarComponentTransformMappingValues( 

1979 VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 

1980 ), 

1981 "rotation": VarComponentTransformMappingValues( 

1982 VarComponentFlags.HAVE_ROTATION, 12, 180, 0 

1983 ), 

1984 "scaleX": VarComponentTransformMappingValues( 

1985 VarComponentFlags.HAVE_SCALE_X, 10, 1, 1 

1986 ), 

1987 "scaleY": VarComponentTransformMappingValues( 

1988 VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1 

1989 ), 

1990 "skewX": VarComponentTransformMappingValues( 

1991 VarComponentFlags.HAVE_SKEW_X, 12, -180, 0 

1992 ), 

1993 "skewY": VarComponentTransformMappingValues( 

1994 VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0 

1995 ), 

1996 "tCenterX": VarComponentTransformMappingValues( 

1997 VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0 

1998 ), 

1999 "tCenterY": VarComponentTransformMappingValues( 

2000 VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0 

2001 ), 

2002} 

2003 

2004 

2005class GlyphVarComponent(object): 

2006 MIN_SIZE = 5 

2007 

2008 def __init__(self): 

2009 self.location = {} 

2010 self.transform = DecomposedTransform() 

2011 

2012 @staticmethod 

2013 def getSize(data): 

2014 size = 5 

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

2016 numAxes = int(data[2]) 

2017 

2018 if flags & VarComponentFlags.GID_IS_24BIT: 

2019 size += 1 

2020 

2021 size += numAxes 

2022 if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: 

2023 size += 2 * numAxes 

2024 else: 

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

2026 size += numAxes 

2027 

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

2029 if flags & mapping_values.flag: 

2030 size += 2 

2031 

2032 return size 

2033 

2034 def decompile(self, data, glyfTable): 

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

2036 self.flags = int(flags) 

2037 data = data[2:] 

2038 

2039 numAxes = int(data[0]) 

2040 data = data[1:] 

2041 

2042 if flags & VarComponentFlags.GID_IS_24BIT: 

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

2044 data = data[3:] 

2045 flags ^= VarComponentFlags.GID_IS_24BIT 

2046 else: 

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

2048 data = data[2:] 

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

2050 

2051 if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: 

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

2053 if sys.byteorder != "big": 

2054 axisIndices.byteswap() 

2055 data = data[2 * numAxes :] 

2056 flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT 

2057 else: 

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

2059 data = data[numAxes:] 

2060 assert len(axisIndices) == numAxes 

2061 axisIndices = list(axisIndices) 

2062 

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

2064 if sys.byteorder != "big": 

2065 axisValues.byteswap() 

2066 data = data[2 * numAxes :] 

2067 assert len(axisValues) == numAxes 

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

2069 

2070 self.location = { 

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

2072 } 

2073 

2074 def read_transform_component(data, values): 

2075 if flags & values.flag: 

2076 return ( 

2077 data[2:], 

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

2079 * values.scale, 

2080 ) 

2081 else: 

2082 return data, values.defaultValue 

2083 

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

2085 data, value = read_transform_component(data, mapping_values) 

2086 setattr(self.transform, attr_name, value) 

2087 

2088 if flags & VarComponentFlags.UNIFORM_SCALE: 

2089 if flags & VarComponentFlags.HAVE_SCALE_X and not ( 

2090 flags & VarComponentFlags.HAVE_SCALE_Y 

2091 ): 

2092 self.transform.scaleY = self.transform.scaleX 

2093 flags |= VarComponentFlags.HAVE_SCALE_Y 

2094 flags ^= VarComponentFlags.UNIFORM_SCALE 

2095 

2096 return data 

2097 

2098 def compile(self, glyfTable): 

2099 data = b"" 

2100 

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

2102 flags = 0 

2103 # Calculate optimal transform component flags 

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

2105 value = getattr(self.transform, attr_name) 

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

2107 mapping.defaultValue / mapping.scale, mapping.fractionalBits 

2108 ): 

2109 flags |= mapping.flag 

2110 else: 

2111 flags = self.flags 

2112 

2113 if ( 

2114 flags & VarComponentFlags.HAVE_SCALE_X 

2115 and flags & VarComponentFlags.HAVE_SCALE_Y 

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

2117 ): 

2118 flags |= VarComponentFlags.UNIFORM_SCALE 

2119 flags ^= VarComponentFlags.HAVE_SCALE_Y 

2120 

2121 numAxes = len(self.location) 

2122 

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

2124 

2125 glyphID = glyfTable.getGlyphID(self.glyphName) 

2126 if glyphID > 65535: 

2127 flags |= VarComponentFlags.GID_IS_24BIT 

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

2129 else: 

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

2131 

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

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

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

2135 else: 

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

2137 if sys.byteorder != "big": 

2138 axisIndices.byteswap() 

2139 flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT 

2140 data = data + bytes(axisIndices) 

2141 

2142 axisValues = self.location.values() 

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

2144 if sys.byteorder != "big": 

2145 axisValues.byteswap() 

2146 data = data + bytes(axisValues) 

2147 

2148 def write_transform_component(data, value, values): 

2149 if flags & values.flag: 

2150 return data + struct.pack( 

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

2152 ) 

2153 else: 

2154 return data 

2155 

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

2157 value = getattr(self.transform, attr_name) 

2158 data = write_transform_component(data, value, mapping_values) 

2159 

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

2161 

2162 def toXML(self, writer, ttFont): 

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

2164 

2165 if hasattr(self, "flags"): 

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

2167 

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

2169 v = getattr(self.transform, attr_name) 

2170 if v != mapping.defaultValue: 

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

2172 

2173 writer.begintag("varComponent", attrs) 

2174 writer.newline() 

2175 

2176 writer.begintag("location") 

2177 writer.newline() 

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

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

2180 writer.newline() 

2181 writer.endtag("location") 

2182 writer.newline() 

2183 

2184 writer.endtag("varComponent") 

2185 writer.newline() 

2186 

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

2188 self.glyphName = attrs["glyphName"] 

2189 

2190 if "flags" in attrs: 

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

2192 

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

2194 if attr_name not in attrs: 

2195 continue 

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

2197 setattr(self.transform, attr_name, v) 

2198 

2199 for c in content: 

2200 if not isinstance(c, tuple): 

2201 continue 

2202 name, attrs, content = c 

2203 if name != "location": 

2204 continue 

2205 for c in content: 

2206 if not isinstance(c, tuple): 

2207 continue 

2208 name, attrs, content = c 

2209 assert name == "axis" 

2210 assert not content 

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

2212 

2213 def getPointCount(self): 

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

2215 

2216 count = 0 

2217 

2218 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 

2219 count += len(self.location) 

2220 

2221 if self.flags & ( 

2222 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 

2223 ): 

2224 count += 1 

2225 if self.flags & VarComponentFlags.HAVE_ROTATION: 

2226 count += 1 

2227 if self.flags & ( 

2228 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 

2229 ): 

2230 count += 1 

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

2232 count += 1 

2233 if self.flags & ( 

2234 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 

2235 ): 

2236 count += 1 

2237 

2238 return count 

2239 

2240 def getCoordinatesAndControls(self): 

2241 coords = [] 

2242 controls = [] 

2243 

2244 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 

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

2246 controls.append(tag) 

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

2248 

2249 if self.flags & ( 

2250 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 

2251 ): 

2252 controls.append("translate") 

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

2254 if self.flags & VarComponentFlags.HAVE_ROTATION: 

2255 controls.append("rotation") 

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

2257 if self.flags & ( 

2258 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 

2259 ): 

2260 controls.append("scale") 

2261 coords.append( 

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

2263 ) 

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

2265 controls.append("skew") 

2266 coords.append( 

2267 ( 

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

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

2270 ) 

2271 ) 

2272 if self.flags & ( 

2273 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 

2274 ): 

2275 controls.append("tCenter") 

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

2277 

2278 return coords, controls 

2279 

2280 def setCoordinates(self, coords): 

2281 i = 0 

2282 

2283 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 

2284 newLocation = {} 

2285 for tag in self.location: 

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

2287 i += 1 

2288 self.location = newLocation 

2289 

2290 self.transform = DecomposedTransform() 

2291 if self.flags & ( 

2292 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 

2293 ): 

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

2295 i += 1 

2296 if self.flags & VarComponentFlags.HAVE_ROTATION: 

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

2298 i += 1 

2299 if self.flags & ( 

2300 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 

2301 ): 

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

2303 coords[i][0], 10 

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

2305 i += 1 

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

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

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

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

2310 ) 

2311 i += 1 

2312 if self.flags & ( 

2313 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 

2314 ): 

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

2316 i += 1 

2317 

2318 return coords[i:] 

2319 

2320 def __eq__(self, other): 

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

2322 return NotImplemented 

2323 return self.__dict__ == other.__dict__ 

2324 

2325 def __ne__(self, other): 

2326 result = self.__eq__(other) 

2327 return result if result is NotImplemented else not result 

2328 

2329 

2330class GlyphCoordinates(object): 

2331 """A list of glyph coordinates. 

2332 

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

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

2335 """ 

2336 

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

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

2339 self.extend(iterable) 

2340 

2341 @property 

2342 def array(self): 

2343 """Returns the underlying array of coordinates""" 

2344 return self._a 

2345 

2346 @staticmethod 

2347 def zeros(count): 

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

2349 g = GlyphCoordinates() 

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

2351 return g 

2352 

2353 def copy(self): 

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

2355 c = GlyphCoordinates() 

2356 c._a.extend(self._a) 

2357 return c 

2358 

2359 def __len__(self): 

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

2361 return len(self._a) // 2 

2362 

2363 def __getitem__(self, k): 

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

2365 a = self._a 

2366 if isinstance(k, slice): 

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

2368 # Instead of calling ourselves recursively, duplicate code; faster 

2369 ret = [] 

2370 for k in indices: 

2371 x = a[2 * k] 

2372 y = a[2 * k + 1] 

2373 ret.append( 

2374 (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y) 

2375 ) 

2376 return ret 

2377 x = a[2 * k] 

2378 y = a[2 * k + 1] 

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

2380 

2381 def __setitem__(self, k, v): 

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

2383 if isinstance(k, slice): 

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

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

2386 for j, i in enumerate(indices): 

2387 self[i] = v[j] 

2388 return 

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

2390 

2391 def __delitem__(self, i): 

2392 """Removes a point from the list""" 

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

2394 del self._a[i] 

2395 del self._a[i] 

2396 

2397 def __repr__(self): 

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

2399 

2400 def append(self, p): 

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

2402 

2403 def extend(self, iterable): 

2404 for p in iterable: 

2405 self._a.extend(p) 

2406 

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

2408 if round is noRound: 

2409 return 

2410 a = self._a 

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

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

2413 

2414 def calcBounds(self): 

2415 a = self._a 

2416 if not a: 

2417 return 0, 0, 0, 0 

2418 xs = a[0::2] 

2419 ys = a[1::2] 

2420 return min(xs), min(ys), max(xs), max(ys) 

2421 

2422 def calcIntBounds(self, round=otRound): 

2423 return tuple(round(v) for v in self.calcBounds()) 

2424 

2425 def relativeToAbsolute(self): 

2426 a = self._a 

2427 x, y = 0, 0 

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

2429 a[i] = x = a[i] + x 

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

2431 

2432 def absoluteToRelative(self): 

2433 a = self._a 

2434 x, y = 0, 0 

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

2436 nx = a[i] 

2437 ny = a[i + 1] 

2438 a[i] = nx - x 

2439 a[i + 1] = ny - y 

2440 x = nx 

2441 y = ny 

2442 

2443 def translate(self, p): 

2444 """ 

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

2446 """ 

2447 x, y = p 

2448 if x == 0 and y == 0: 

2449 return 

2450 a = self._a 

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

2452 a[i] += x 

2453 a[i + 1] += y 

2454 

2455 def scale(self, p): 

2456 """ 

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

2458 """ 

2459 x, y = p 

2460 if x == 1 and y == 1: 

2461 return 

2462 a = self._a 

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

2464 a[i] *= x 

2465 a[i + 1] *= y 

2466 

2467 def transform(self, t): 

2468 """ 

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

2470 """ 

2471 a = self._a 

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

2473 x = a[i] 

2474 y = a[i + 1] 

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

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

2477 a[i] = px 

2478 a[i + 1] = py 

2479 

2480 def __eq__(self, other): 

2481 """ 

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

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

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

2485 >>> g == g2 

2486 True 

2487 >>> g == g3 

2488 False 

2489 >>> g2 == g3 

2490 False 

2491 """ 

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

2493 return NotImplemented 

2494 return self._a == other._a 

2495 

2496 def __ne__(self, other): 

2497 """ 

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

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

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

2501 >>> g != g2 

2502 False 

2503 >>> g != g3 

2504 True 

2505 >>> g2 != g3 

2506 True 

2507 """ 

2508 result = self.__eq__(other) 

2509 return result if result is NotImplemented else not result 

2510 

2511 # Math operations 

2512 

2513 def __pos__(self): 

2514 """ 

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

2516 >>> g 

2517 GlyphCoordinates([(1, 2)]) 

2518 >>> g2 = +g 

2519 >>> g2 

2520 GlyphCoordinates([(1, 2)]) 

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

2522 >>> g2 

2523 GlyphCoordinates([(2, 2)]) 

2524 >>> g 

2525 GlyphCoordinates([(1, 2)]) 

2526 """ 

2527 return self.copy() 

2528 

2529 def __neg__(self): 

2530 """ 

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

2532 >>> g 

2533 GlyphCoordinates([(1, 2)]) 

2534 >>> g2 = -g 

2535 >>> g2 

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

2537 >>> g 

2538 GlyphCoordinates([(1, 2)]) 

2539 """ 

2540 r = self.copy() 

2541 a = r._a 

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

2543 a[i] = -a[i] 

2544 return r 

2545 

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

2547 r = self.copy() 

2548 r.toInt(round=round) 

2549 return r 

2550 

2551 def __add__(self, other): 

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

2553 

2554 def __sub__(self, other): 

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

2556 

2557 def __mul__(self, other): 

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

2559 

2560 def __truediv__(self, other): 

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

2562 

2563 __radd__ = __add__ 

2564 __rmul__ = __mul__ 

2565 

2566 def __rsub__(self, other): 

2567 return other + (-self) 

2568 

2569 def __iadd__(self, other): 

2570 """ 

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

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

2573 >>> g 

2574 GlyphCoordinates([(1.5, 2)]) 

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

2576 >>> g += g2 

2577 >>> g 

2578 GlyphCoordinates([(4.5, 6)]) 

2579 """ 

2580 if isinstance(other, tuple): 

2581 assert len(other) == 2 

2582 self.translate(other) 

2583 return self 

2584 if isinstance(other, GlyphCoordinates): 

2585 other = other._a 

2586 a = self._a 

2587 assert len(a) == len(other) 

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

2589 a[i] += other[i] 

2590 return self 

2591 return NotImplemented 

2592 

2593 def __isub__(self, other): 

2594 """ 

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

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

2597 >>> g 

2598 GlyphCoordinates([(0.5, 2)]) 

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

2600 >>> g -= g2 

2601 >>> g 

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

2603 """ 

2604 if isinstance(other, tuple): 

2605 assert len(other) == 2 

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

2607 return self 

2608 if isinstance(other, GlyphCoordinates): 

2609 other = other._a 

2610 a = self._a 

2611 assert len(a) == len(other) 

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

2613 a[i] -= other[i] 

2614 return self 

2615 return NotImplemented 

2616 

2617 def __imul__(self, other): 

2618 """ 

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

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

2621 >>> g *= 2 

2622 >>> g 

2623 GlyphCoordinates([(4, 2)]) 

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

2625 >>> g *= 2 

2626 >>> g 

2627 GlyphCoordinates([(2, 4)]) 

2628 """ 

2629 if isinstance(other, tuple): 

2630 assert len(other) == 2 

2631 self.scale(other) 

2632 return self 

2633 if isinstance(other, Number): 

2634 if other == 1: 

2635 return self 

2636 a = self._a 

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

2638 a[i] *= other 

2639 return self 

2640 return NotImplemented 

2641 

2642 def __itruediv__(self, other): 

2643 """ 

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

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

2646 >>> g /= 2 

2647 >>> g 

2648 GlyphCoordinates([(1, 1)]) 

2649 """ 

2650 if isinstance(other, Number): 

2651 other = (other, other) 

2652 if isinstance(other, tuple): 

2653 if other == (1, 1): 

2654 return self 

2655 assert len(other) == 2 

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

2657 return self 

2658 return NotImplemented 

2659 

2660 def __bool__(self): 

2661 """ 

2662 >>> g = GlyphCoordinates([]) 

2663 >>> bool(g) 

2664 False 

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

2666 >>> bool(g) 

2667 True 

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

2669 >>> bool(g) 

2670 True 

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

2672 >>> bool(g) 

2673 True 

2674 """ 

2675 return bool(self._a) 

2676 

2677 __nonzero__ = __bool__ 

2678 

2679 

2680if __name__ == "__main__": 

2681 import doctest, sys 

2682 

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