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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1395 statements  

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 # 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 loca = ttFont["loca"] 

95 pos = int(loca[0]) 

96 nextPos = 0 

97 noname = 0 

98 self.glyphs = {} 

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

100 self._reverseGlyphOrder = {} 

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

102 try: 

103 glyphName = glyphOrder[i] 

104 except IndexError: 

105 noname = noname + 1 

106 glyphName = "ttxautoglyph%s" % i 

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

108 glyphdata = data[pos:nextPos] 

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

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

111 glyph = Glyph(glyphdata) 

112 self.glyphs[glyphName] = glyph 

113 pos = nextPos 

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

115 log.warning( 

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

117 nextPos, 

118 len(data), 

119 ) 

120 if noname: 

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

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

123 self.ensureDecompiled() 

124 

125 def ensureDecompiled(self, recurse=False): 

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

127 # ensureDecompiled across the library. 

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

129 glyph.expand(self) 

130 

131 def compile(self, ttFont): 

132 optimizeSpeed = ttFont.cfg[ttLib.OPTIMIZE_FONT_SPEED] 

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

134 self.glyphOrder = ttFont.getGlyphOrder() 

135 padding = self.padding 

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

137 locations = [] 

138 currentLocation = 0 

139 dataList = [] 

140 recalcBBoxes = ttFont.recalcBBoxes 

141 boundsDone = set() 

142 for glyphName in self.glyphOrder: 

143 glyph = self.glyphs[glyphName] 

144 glyphData = glyph.compile( 

145 self, 

146 recalcBBoxes, 

147 boundsDone=boundsDone, 

148 optimizeSize=not optimizeSpeed, 

149 ) 

150 if padding > 1: 

151 glyphData = pad(glyphData, size=padding) 

152 locations.append(currentLocation) 

153 currentLocation = currentLocation + len(glyphData) 

154 dataList.append(glyphData) 

155 locations.append(currentLocation) 

156 

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

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

159 # table to use the short offsets. 

160 indices = [ 

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

162 ] 

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

164 # It fits. Do it. 

165 for i in indices: 

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

167 currentLocation = 0 

168 for i, glyphData in enumerate(dataList): 

169 locations[i] = currentLocation 

170 currentLocation += len(glyphData) 

171 locations[len(dataList)] = currentLocation 

172 

173 data = b"".join(dataList) 

174 if "loca" in ttFont: 

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

176 if "maxp" in ttFont: 

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

178 if not data: 

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

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

181 # on Windows as well. 

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

183 data = b"\0" 

184 return data 

185 

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

187 notice = ( 

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

189 "will be recalculated by the compiler." 

190 ) 

191 glyphNames = ttFont.getGlyphNames() 

192 if not splitGlyphs: 

193 writer.newline() 

194 writer.comment(notice) 

195 writer.newline() 

196 writer.newline() 

197 numGlyphs = len(glyphNames) 

198 if splitGlyphs: 

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

200 existingGlyphFiles = set() 

201 for glyphName in glyphNames: 

202 glyph = self.get(glyphName) 

203 if glyph is None: 

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

205 continue 

206 if glyph.numberOfContours: 

207 if splitGlyphs: 

208 glyphPath = userNameToFileName( 

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

210 existingGlyphFiles, 

211 prefix=path + ".", 

212 suffix=ext, 

213 ) 

214 existingGlyphFiles.add(glyphPath.lower()) 

215 glyphWriter = xmlWriter.XMLWriter( 

216 glyphPath, 

217 idlefunc=writer.idlefunc, 

218 newlinestr=writer.newlinestr, 

219 ) 

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

221 glyphWriter.newline() 

222 glyphWriter.begintag("glyf") 

223 glyphWriter.newline() 

224 glyphWriter.comment(notice) 

225 glyphWriter.newline() 

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

227 else: 

228 glyphWriter = writer 

229 glyphWriter.begintag( 

230 "TTGlyph", 

231 [ 

232 ("name", glyphName), 

233 ("xMin", glyph.xMin), 

234 ("yMin", glyph.yMin), 

235 ("xMax", glyph.xMax), 

236 ("yMax", glyph.yMax), 

237 ], 

238 ) 

239 glyphWriter.newline() 

240 glyph.toXML(glyphWriter, ttFont) 

241 glyphWriter.endtag("TTGlyph") 

242 glyphWriter.newline() 

243 if splitGlyphs: 

244 glyphWriter.endtag("glyf") 

245 glyphWriter.newline() 

246 glyphWriter.endtag("ttFont") 

247 glyphWriter.newline() 

248 glyphWriter.close() 

249 else: 

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

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

252 if not splitGlyphs: 

253 writer.newline() 

254 writer.newline() 

255 

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

257 if name != "TTGlyph": 

258 return 

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

260 self.glyphs = {} 

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

262 self.glyphOrder = ttFont.getGlyphOrder() 

263 glyphName = attrs["name"] 

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

265 glyph = Glyph() 

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

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

268 self.glyphs[glyphName] = glyph 

269 for element in content: 

270 if not isinstance(element, tuple): 

271 continue 

272 name, attrs, content = element 

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

274 if not ttFont.recalcBBoxes: 

275 glyph.compact(self, 0) 

276 

277 def setGlyphOrder(self, glyphOrder): 

278 """Sets the glyph order 

279 

280 Args: 

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

282 """ 

283 self.glyphOrder = glyphOrder 

284 self._reverseGlyphOrder = {} 

285 

286 def getGlyphName(self, glyphID): 

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

288 

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

290 """ 

291 return self.glyphOrder[glyphID] 

292 

293 def _buildReverseGlyphOrderDict(self): 

294 self._reverseGlyphOrder = d = {} 

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

296 d[glyphName] = glyphID 

297 

298 def getGlyphID(self, glyphName): 

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

300 

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

302 """ 

303 glyphOrder = self.glyphOrder 

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

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

306 self._buildReverseGlyphOrderDict() 

307 id = self._reverseGlyphOrder.get(glyphName) 

308 if id is None: 

309 raise ValueError(glyphName) 

310 return id 

311 

312 def removeHinting(self): 

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

314 

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

316 """ 

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

318 glyph.removeHinting() 

319 

320 def keys(self): 

321 return self.glyphs.keys() 

322 

323 def has_key(self, glyphName): 

324 return glyphName in self.glyphs 

325 

326 __contains__ = has_key 

327 

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

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

330 if glyph is not None: 

331 glyph.expand(self) 

332 return glyph 

333 

334 def __getitem__(self, glyphName): 

335 glyph = self.glyphs[glyphName] 

336 glyph.expand(self) 

337 return glyph 

338 

339 def __setitem__(self, glyphName, glyph): 

340 self.glyphs[glyphName] = glyph 

341 if glyphName not in self.glyphOrder: 

342 self.glyphOrder.append(glyphName) 

343 

344 def __delitem__(self, glyphName): 

345 del self.glyphs[glyphName] 

346 self.glyphOrder.remove(glyphName) 

347 

348 def __len__(self): 

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

350 return len(self.glyphs) 

351 

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

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

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

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

356 

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

358 

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

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

361 

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

363 """ 

364 glyph = self[glyphName] 

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

366 glyph.recalcBounds(self) 

367 

368 horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName] 

369 leftSideX = glyph.xMin - leftSideBearing 

370 rightSideX = leftSideX + horizontalAdvanceWidth 

371 

372 if vMetrics: 

373 verticalAdvanceWidth, topSideBearing = vMetrics[glyphName] 

374 topSideY = topSideBearing + glyph.yMax 

375 bottomSideY = topSideY - verticalAdvanceWidth 

376 else: 

377 bottomSideY = topSideY = 0 

378 

379 return [ 

380 (leftSideX, 0), 

381 (rightSideX, 0), 

382 (0, topSideY), 

383 (0, bottomSideY), 

384 ] 

385 

386 def _getCoordinatesAndControls( 

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

388 ): 

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

390 

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

392 as mandated by the "gvar" spec. 

393 

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

395 - numberOfContours: -1 for composite glyphs. 

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

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

398 optimization). 

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

400 composite glyphs). 

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

402 composite glyphs (None for simple glyphs). 

403 

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

405 the "_getPhantomPoints" method). 

406 

407 Return None if the requested glyphName is not present. 

408 """ 

409 glyph = self.get(glyphName) 

410 if glyph is None: 

411 return None 

412 if glyph.isComposite(): 

413 coords = GlyphCoordinates( 

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

415 ) 

416 controls = _GlyphControls( 

417 numberOfContours=glyph.numberOfContours, 

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

419 flags=None, 

420 components=[ 

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

422 for c in glyph.components 

423 ], 

424 ) 

425 else: 

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

427 coords = coords.copy() 

428 controls = _GlyphControls( 

429 numberOfContours=glyph.numberOfContours, 

430 endPts=endPts, 

431 flags=flags, 

432 components=None, 

433 ) 

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

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

436 coords.extend(phantomPoints) 

437 coords.toInt(round=round) 

438 return coords, controls 

439 

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

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

442 

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

444 points" as the last four coordinates. 

445 

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

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

448 the glyph's bounding boxes. 

449 

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

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

452 method). 

453 """ 

454 glyph = self[glyphName] 

455 

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

457 assert len(coord) >= 4 

458 leftSideX = coord[-4][0] 

459 rightSideX = coord[-3][0] 

460 topSideY = coord[-2][1] 

461 bottomSideY = coord[-1][1] 

462 

463 coord = coord[:-4] 

464 

465 if glyph.isComposite(): 

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

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

468 if hasattr(comp, "x"): 

469 comp.x, comp.y = p 

470 elif glyph.numberOfContours == 0: 

471 assert len(coord) == 0 

472 else: 

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

474 glyph.coordinates = GlyphCoordinates(coord) 

475 

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

477 

478 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 

479 if horizontalAdvanceWidth < 0: 

480 # unlikely, but it can happen, see: 

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

482 horizontalAdvanceWidth = 0 

483 leftSideBearing = otRound(glyph.xMin - leftSideX) 

484 hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing 

485 

486 if vMetrics is not None: 

487 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 

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

489 verticalAdvanceWidth = 0 

490 topSideBearing = otRound(topSideY - glyph.yMax) 

491 vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing 

492 

493 # Deprecated 

494 

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

496 """This method is wrong and deprecated. 

497 For rationale see: 

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

499 """ 

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

501 if vMetrics is None: 

502 verticalAdvanceWidth = ttFont["head"].unitsPerEm 

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

504 if topSideY is None: 

505 if defaultVerticalOrigin is not None: 

506 topSideY = defaultVerticalOrigin 

507 else: 

508 topSideY = verticalAdvanceWidth 

509 glyph = self[glyphName] 

510 glyph.recalcBounds(self) 

511 topSideBearing = otRound(topSideY - glyph.yMax) 

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

513 return vMetrics 

514 

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

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

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

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

519 hMetrics = ttFont["hmtx"].metrics 

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

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

522 

523 @deprecateFunction( 

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

525 ) 

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

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

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

529 hMetrics = ttFont["hmtx"].metrics 

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

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

532 

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

534 def setCoordinates(self, glyphName, ttFont): 

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

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

537 hMetrics = ttFont["hmtx"].metrics 

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

539 self._setCoordinates(glyphName, hMetrics, vMetrics) 

540 

541 

542_GlyphControls = namedtuple( 

543 "_GlyphControls", "numberOfContours endPts flags components" 

544) 

545 

546 

547glyphHeaderFormat = """ 

548 > # big endian 

549 numberOfContours: h 

550 xMin: h 

551 yMin: h 

552 xMax: h 

553 yMax: h 

554""" 

555 

556# flags 

557flagOnCurve = 0x01 

558flagXShort = 0x02 

559flagYShort = 0x04 

560flagRepeat = 0x08 

561flagXsame = 0x10 

562flagYsame = 0x20 

563flagOverlapSimple = 0x40 

564flagCubic = 0x80 

565 

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

567keepFlags = flagOnCurve + flagOverlapSimple + flagCubic 

568 

569_flagSignBytes = { 

570 0: 2, 

571 flagXsame: 0, 

572 flagXShort | flagXsame: +1, 

573 flagXShort: -1, 

574 flagYsame: 0, 

575 flagYShort | flagYsame: +1, 

576 flagYShort: -1, 

577} 

578 

579 

580def flagBest(x, y, onCurve): 

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

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

583 

584 flag = flagOnCurve if onCurve else 0 

585 cost = 0 

586 # do x 

587 if x == 0: 

588 flag = flag | flagXsame 

589 elif -255 <= x <= 255: 

590 flag = flag | flagXShort 

591 if x > 0: 

592 flag = flag | flagXsame 

593 cost += 1 

594 else: 

595 cost += 2 

596 # do y 

597 if y == 0: 

598 flag = flag | flagYsame 

599 elif -255 <= y <= 255: 

600 flag = flag | flagYShort 

601 if y > 0: 

602 flag = flag | flagYsame 

603 cost += 1 

604 else: 

605 cost += 2 

606 return flag, cost 

607 

608 

609def flagFits(newFlag, oldFlag, mask): 

610 newBytes = _flagSignBytes[newFlag & mask] 

611 oldBytes = _flagSignBytes[oldFlag & mask] 

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

613 

614 

615def flagSupports(newFlag, oldFlag): 

616 return ( 

617 (oldFlag & flagOnCurve) == (newFlag & flagOnCurve) 

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

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

620 ) 

621 

622 

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

624 byteCount = _flagSignBytes[flag & mask] 

625 if byteCount == 1: 

626 coordBytes.append(coord) 

627 elif byteCount == -1: 

628 coordBytes.append(-coord) 

629 elif byteCount == 2: 

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

631 

632 

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

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

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

636 

637 

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

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

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

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

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

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

644WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy 

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

646WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow 

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

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

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

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

651 

652 

653CompositeMaxpValues = namedtuple( 

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

655) 

656 

657 

658class Glyph(object): 

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

660 

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

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

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

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

665 attributes. 

666 

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

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

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

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

671 access its data. 

672 

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

674 

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

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

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

678 

679 """ 

680 

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

682 if not data: 

683 # empty char 

684 self.numberOfContours = 0 

685 return 

686 self.data = data 

687 

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

689 data = self.compile(glyfTable, recalcBBoxes) 

690 self.__dict__.clear() 

691 self.data = data 

692 

693 def expand(self, glyfTable): 

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

695 # already unpacked 

696 return 

697 if not self.data: 

698 # empty char 

699 del self.data 

700 self.numberOfContours = 0 

701 return 

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

703 del self.data 

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

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

706 # one, so short-circuit here. 

707 if self.numberOfContours == 0: 

708 return 

709 if self.isComposite(): 

710 self.decompileComponents(data, glyfTable) 

711 else: 

712 self.decompileCoordinates(data) 

713 

714 def compile( 

715 self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=True 

716 ): 

717 if hasattr(self, "data"): 

718 if recalcBBoxes: 

719 # must unpack glyph in order to recalculate bounding box 

720 self.expand(glyfTable) 

721 else: 

722 return self.data 

723 if self.numberOfContours == 0: 

724 return b"" 

725 

726 if recalcBBoxes: 

727 self.recalcBounds(glyfTable, boundsDone=boundsDone) 

728 

729 data = sstruct.pack(glyphHeaderFormat, self) 

730 if self.isComposite(): 

731 data = data + self.compileComponents(glyfTable) 

732 else: 

733 data = data + self.compileCoordinates(optimizeSize=optimizeSize) 

734 return data 

735 

736 def toXML(self, writer, ttFont): 

737 if self.isComposite(): 

738 for compo in self.components: 

739 compo.toXML(writer, ttFont) 

740 haveInstructions = hasattr(self, "program") 

741 else: 

742 last = 0 

743 for i in range(self.numberOfContours): 

744 writer.begintag("contour") 

745 writer.newline() 

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

747 attrs = [ 

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

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

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

751 ] 

752 if self.flags[j] & flagOverlapSimple: 

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

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

755 if self.flags[j] & flagCubic: 

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

757 writer.simpletag("pt", attrs) 

758 writer.newline() 

759 last = self.endPtsOfContours[i] + 1 

760 writer.endtag("contour") 

761 writer.newline() 

762 haveInstructions = self.numberOfContours > 0 

763 if haveInstructions: 

764 if self.program: 

765 writer.begintag("instructions") 

766 writer.newline() 

767 self.program.toXML(writer, ttFont) 

768 writer.endtag("instructions") 

769 else: 

770 writer.simpletag("instructions") 

771 writer.newline() 

772 

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

774 if name == "contour": 

775 if self.numberOfContours < 0: 

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

777 self.numberOfContours = self.numberOfContours + 1 

778 coordinates = GlyphCoordinates() 

779 flags = bytearray() 

780 for element in content: 

781 if not isinstance(element, tuple): 

782 continue 

783 name, attrs, content = element 

784 if name != "pt": 

785 continue # ignore anything but "pt" 

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

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

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

789 flag |= flagOverlapSimple 

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

791 flag |= flagCubic 

792 flags.append(flag) 

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

794 self.coordinates = coordinates 

795 self.flags = flags 

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

797 else: 

798 self.coordinates.extend(coordinates) 

799 self.flags.extend(flags) 

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

801 elif name == "component": 

802 if self.numberOfContours > 0: 

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

804 self.numberOfContours = -1 

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

806 self.components = [] 

807 component = GlyphComponent() 

808 self.components.append(component) 

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

810 elif name == "instructions": 

811 self.program = ttProgram.Program() 

812 for element in content: 

813 if not isinstance(element, tuple): 

814 continue 

815 name, attrs, content = element 

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

817 

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

819 assert self.isComposite() 

820 nContours = 0 

821 nPoints = 0 

822 initialMaxComponentDepth = maxComponentDepth 

823 for compo in self.components: 

824 baseGlyph = glyfTable[compo.glyphName] 

825 if baseGlyph.numberOfContours == 0: 

826 continue 

827 elif baseGlyph.numberOfContours > 0: 

828 nP, nC = baseGlyph.getMaxpValues() 

829 else: 

830 nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues( 

831 glyfTable, initialMaxComponentDepth + 1 

832 ) 

833 maxComponentDepth = max(maxComponentDepth, componentDepth) 

834 nPoints = nPoints + nP 

835 nContours = nContours + nC 

836 return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) 

837 

838 def getMaxpValues(self): 

839 assert self.numberOfContours > 0 

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

841 

842 def decompileComponents(self, data, glyfTable): 

843 self.components = [] 

844 more = 1 

845 haveInstructions = 0 

846 while more: 

847 component = GlyphComponent() 

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

849 haveInstructions = haveInstructions | haveInstr 

850 self.components.append(component) 

851 if haveInstructions: 

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

853 data = data[2:] 

854 self.program = ttProgram.Program() 

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

856 data = data[numInstructions:] 

857 if len(data) >= 4: 

858 log.warning( 

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

860 len(data), 

861 ) 

862 

863 def decompileCoordinates(self, data): 

864 endPtsOfContours = array.array("H") 

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

866 if sys.byteorder != "big": 

867 endPtsOfContours.byteswap() 

868 self.endPtsOfContours = endPtsOfContours.tolist() 

869 

870 pos = 2 * self.numberOfContours 

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

872 self.program = ttProgram.Program() 

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

874 pos += 2 + instructionLength 

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

876 flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw( 

877 nCoordinates, data, pos 

878 ) 

879 

880 # fill in repetitions and apply signs 

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

882 xIndex = 0 

883 yIndex = 0 

884 for i in range(nCoordinates): 

885 flag = flags[i] 

886 # x coordinate 

887 if flag & flagXShort: 

888 if flag & flagXsame: 

889 x = xCoordinates[xIndex] 

890 else: 

891 x = -xCoordinates[xIndex] 

892 xIndex = xIndex + 1 

893 elif flag & flagXsame: 

894 x = 0 

895 else: 

896 x = xCoordinates[xIndex] 

897 xIndex = xIndex + 1 

898 # y coordinate 

899 if flag & flagYShort: 

900 if flag & flagYsame: 

901 y = yCoordinates[yIndex] 

902 else: 

903 y = -yCoordinates[yIndex] 

904 yIndex = yIndex + 1 

905 elif flag & flagYsame: 

906 y = 0 

907 else: 

908 y = yCoordinates[yIndex] 

909 yIndex = yIndex + 1 

910 coordinates[i] = (x, y) 

911 assert xIndex == len(xCoordinates) 

912 assert yIndex == len(yCoordinates) 

913 coordinates.relativeToAbsolute() 

914 # discard all flags except "keepFlags" 

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

916 flags[i] &= keepFlags 

917 self.flags = flags 

918 

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

920 # unpack flags and prepare unpacking of coordinates 

921 flags = bytearray(nCoordinates) 

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

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

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

925 xFormat = ">" # big endian 

926 yFormat = ">" # big endian 

927 j = 0 

928 while True: 

929 flag = data[pos] 

930 pos += 1 

931 repeat = 1 

932 if flag & flagRepeat: 

933 repeat = data[pos] + 1 

934 pos += 1 

935 for k in range(repeat): 

936 if flag & flagXShort: 

937 xFormat = xFormat + "B" 

938 elif not (flag & flagXsame): 

939 xFormat = xFormat + "h" 

940 if flag & flagYShort: 

941 yFormat = yFormat + "B" 

942 elif not (flag & flagYsame): 

943 yFormat = yFormat + "h" 

944 flags[j] = flag 

945 j = j + 1 

946 if j >= nCoordinates: 

947 break 

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

949 # unpack raw coordinates, krrrrrr-tching! 

950 xDataLen = struct.calcsize(xFormat) 

951 yDataLen = struct.calcsize(yFormat) 

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

953 log.warning( 

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

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

956 ) 

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

958 yCoordinates = struct.unpack( 

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

960 ) 

961 return flags, xCoordinates, yCoordinates 

962 

963 def compileComponents(self, glyfTable): 

964 data = b"" 

965 lastcomponent = len(self.components) - 1 

966 more = 1 

967 haveInstructions = 0 

968 for i, compo in enumerate(self.components): 

969 if i == lastcomponent: 

970 haveInstructions = hasattr(self, "program") 

971 more = 0 

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

973 if haveInstructions: 

974 instructions = self.program.getBytecode() 

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

976 return data 

977 

978 def compileCoordinates(self, *, optimizeSize=True): 

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

980 data = [] 

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

982 if sys.byteorder != "big": 

983 endPtsOfContours.byteswap() 

984 data.append(endPtsOfContours.tobytes()) 

985 instructions = self.program.getBytecode() 

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

987 data.append(instructions) 

988 

989 deltas = self.coordinates.copy() 

990 deltas.toInt() 

991 deltas.absoluteToRelative() 

992 

993 if optimizeSize: 

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

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

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

997 else: 

998 deltas = self.compileDeltasForSpeed(self.flags, deltas) 

999 

1000 data.extend(deltas) 

1001 return b"".join(data) 

1002 

1003 def compileDeltasGreedy(self, flags, deltas): 

1004 # Implements greedy algorithm for packing coordinate deltas: 

1005 # uses shortest representation one coordinate at a time. 

1006 compressedFlags = bytearray() 

1007 compressedXs = bytearray() 

1008 compressedYs = bytearray() 

1009 lastflag = None 

1010 repeat = 0 

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

1012 # Oh, the horrors of TrueType 

1013 # do x 

1014 if x == 0: 

1015 flag = flag | flagXsame 

1016 elif -255 <= x <= 255: 

1017 flag = flag | flagXShort 

1018 if x > 0: 

1019 flag = flag | flagXsame 

1020 else: 

1021 x = -x 

1022 compressedXs.append(x) 

1023 else: 

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

1025 # do y 

1026 if y == 0: 

1027 flag = flag | flagYsame 

1028 elif -255 <= y <= 255: 

1029 flag = flag | flagYShort 

1030 if y > 0: 

1031 flag = flag | flagYsame 

1032 else: 

1033 y = -y 

1034 compressedYs.append(y) 

1035 else: 

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

1037 # handle repeating flags 

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

1039 repeat = repeat + 1 

1040 if repeat == 1: 

1041 compressedFlags.append(flag) 

1042 else: 

1043 compressedFlags[-2] = flag | flagRepeat 

1044 compressedFlags[-1] = repeat 

1045 else: 

1046 repeat = 0 

1047 compressedFlags.append(flag) 

1048 lastflag = flag 

1049 return (compressedFlags, compressedXs, compressedYs) 

1050 

1051 def compileDeltasOptimal(self, flags, deltas): 

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

1053 # deltas. The savings are negligible :(. 

1054 candidates = [] 

1055 bestTuple = None 

1056 bestCost = 0 

1057 repeat = 0 

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

1059 # Oh, the horrors of TrueType 

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

1061 bestCost += 1 + coordBytes 

1062 newCandidates = [ 

1063 (bestCost, bestTuple, flag, coordBytes), 

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

1065 ] 

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

1067 if ( 

1068 lastCost + coordBytes <= bestCost + 1 

1069 and (lastFlag & flagRepeat) 

1070 and (lastFlag < 0xFF00) 

1071 and flagSupports(lastFlag, flag) 

1072 ): 

1073 if (lastFlag & 0xFF) == ( 

1074 flag | flagRepeat 

1075 ) and lastCost == bestCost + 1: 

1076 continue 

1077 newCandidates.append( 

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

1079 ) 

1080 candidates = newCandidates 

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

1082 bestCost = bestTuple[0] 

1083 

1084 flags = [] 

1085 while bestTuple: 

1086 cost, bestTuple, flag, coordBytes = bestTuple 

1087 flags.append(flag) 

1088 flags.reverse() 

1089 

1090 compressedFlags = bytearray() 

1091 compressedXs = bytearray() 

1092 compressedYs = bytearray() 

1093 coords = iter(deltas) 

1094 ff = [] 

1095 for flag in flags: 

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

1097 compressedFlags.append(flag) 

1098 if flag & flagRepeat: 

1099 assert repeatCount > 0 

1100 compressedFlags.append(repeatCount) 

1101 else: 

1102 assert repeatCount == 0 

1103 for i in range(1 + repeatCount): 

1104 x, y = next(coords) 

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

1106 ff.append(flag) 

1107 try: 

1108 next(coords) 

1109 raise Exception("internal error") 

1110 except StopIteration: 

1111 pass 

1112 

1113 return (compressedFlags, compressedXs, compressedYs) 

1114 

1115 def compileDeltasForSpeed(self, flags, deltas): 

1116 # uses widest representation needed, for all deltas. 

1117 compressedFlags = bytearray() 

1118 compressedXs = bytearray() 

1119 compressedYs = bytearray() 

1120 

1121 # Compute the necessary width for each axis 

1122 xs = [d[0] for d in deltas] 

1123 ys = [d[1] for d in deltas] 

1124 minX, minY, maxX, maxY = min(xs), min(ys), max(xs), max(ys) 

1125 xZero = minX == 0 and maxX == 0 

1126 yZero = minY == 0 and maxY == 0 

1127 xShort = -255 <= minX <= maxX <= 255 

1128 yShort = -255 <= minY <= maxY <= 255 

1129 

1130 lastflag = None 

1131 repeat = 0 

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

1133 # Oh, the horrors of TrueType 

1134 # do x 

1135 if xZero: 

1136 flag = flag | flagXsame 

1137 elif xShort: 

1138 flag = flag | flagXShort 

1139 if x > 0: 

1140 flag = flag | flagXsame 

1141 else: 

1142 x = -x 

1143 compressedXs.append(x) 

1144 else: 

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

1146 # do y 

1147 if yZero: 

1148 flag = flag | flagYsame 

1149 elif yShort: 

1150 flag = flag | flagYShort 

1151 if y > 0: 

1152 flag = flag | flagYsame 

1153 else: 

1154 y = -y 

1155 compressedYs.append(y) 

1156 else: 

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

1158 # handle repeating flags 

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

1160 repeat = repeat + 1 

1161 if repeat == 1: 

1162 compressedFlags.append(flag) 

1163 else: 

1164 compressedFlags[-2] = flag | flagRepeat 

1165 compressedFlags[-1] = repeat 

1166 else: 

1167 repeat = 0 

1168 compressedFlags.append(flag) 

1169 lastflag = flag 

1170 return (compressedFlags, compressedXs, compressedYs) 

1171 

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

1173 """Recalculates the bounds of the glyph. 

1174 

1175 Each glyph object stores its bounding box in the 

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

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

1178 must be provided to resolve component bounds. 

1179 """ 

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

1181 glyfTable, boundsDone=boundsDone 

1182 ): 

1183 return 

1184 try: 

1185 coords, endPts, flags = self.getCoordinates(glyfTable, round=otRound) 

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

1187 except NotImplementedError: 

1188 pass 

1189 

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

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

1192 certain constrained properties. Namely, none of the components 

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

1194 uses the anchor points. 

1195 

1196 Each glyph object stores its bounding box in the 

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

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

1199 must be provided to resolve component bounds. 

1200 

1201 Return True if bounds were calculated, False otherwise. 

1202 """ 

1203 for compo in self.components: 

1204 if not compo._hasOnlyIntegerTranslate(): 

1205 return False 

1206 

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

1208 bounds = None 

1209 for compo in self.components: 

1210 glyphName = compo.glyphName 

1211 g = glyfTable[glyphName] 

1212 

1213 if boundsDone is None or glyphName not in boundsDone: 

1214 g.recalcBounds(glyfTable, boundsDone=boundsDone) 

1215 if boundsDone is not None: 

1216 boundsDone.add(glyphName) 

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

1218 if g.yMin == g.yMax and g.xMin == g.xMax: 

1219 continue 

1220 

1221 x, y = compo.x, compo.y 

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

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

1224 

1225 if bounds is None: 

1226 bounds = (0, 0, 0, 0) 

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

1228 return True 

1229 

1230 def isComposite(self): 

1231 """Test whether a glyph has components""" 

1232 if hasattr(self, "data"): 

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

1234 else: 

1235 return self.numberOfContours == -1 

1236 

1237 def getCoordinates(self, glyfTable, *, round=noRound): 

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

1239 

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

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

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

1243 

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

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

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

1247 in the glyph. 

1248 

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

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

1251 """ 

1252 

1253 if self.numberOfContours > 0: 

1254 return self.coordinates, self.endPtsOfContours, self.flags 

1255 elif self.isComposite(): 

1256 # it's a composite 

1257 allCoords = GlyphCoordinates() 

1258 allFlags = bytearray() 

1259 allEndPts = [] 

1260 for compo in self.components: 

1261 g = glyfTable[compo.glyphName] 

1262 try: 

1263 coordinates, endPts, flags = g.getCoordinates( 

1264 glyfTable, round=round 

1265 ) 

1266 except RecursionError: 

1267 raise ttLib.TTLibError( 

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

1269 % compo.glyphName 

1270 ) 

1271 coordinates = GlyphCoordinates(coordinates) 

1272 # if asked to round e.g. while computing bboxes, it's important we 

1273 # do it immediately before a component transform is applied to a 

1274 # simple glyph's coordinates in case these might still contain floats; 

1275 # however, if the referenced component glyph is another composite, we 

1276 # must not round here but only at the end, after all the nested 

1277 # transforms have been applied, or else rounding errors will compound. 

1278 if round is not noRound and g.numberOfContours > 0: 

1279 coordinates.toInt(round=round) 

1280 if hasattr(compo, "firstPt"): 

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

1282 # computing the offset between the points 

1283 if hasattr(compo, "transform"): 

1284 coordinates.transform(compo.transform) 

1285 x1, y1 = allCoords[compo.firstPt] 

1286 x2, y2 = coordinates[compo.secondPt] 

1287 move = x1 - x2, y1 - y2 

1288 coordinates.translate(move) 

1289 else: 

1290 # component uses XY offsets 

1291 move = compo.x, compo.y 

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

1293 coordinates.translate(move) 

1294 else: 

1295 apple_way = compo.flags & SCALED_COMPONENT_OFFSET 

1296 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET 

1297 assert not (apple_way and ms_way) 

1298 if not (apple_way or ms_way): 

1299 scale_component_offset = ( 

1300 SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file 

1301 ) 

1302 else: 

1303 scale_component_offset = apple_way 

1304 if scale_component_offset: 

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

1306 coordinates.translate(move) 

1307 coordinates.transform(compo.transform) 

1308 else: 

1309 # the MS way: first scale, then move 

1310 coordinates.transform(compo.transform) 

1311 coordinates.translate(move) 

1312 offset = len(allCoords) 

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

1314 allCoords.extend(coordinates) 

1315 allFlags.extend(flags) 

1316 return allCoords, allEndPts, allFlags 

1317 else: 

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

1319 

1320 def getComponentNames(self, glyfTable): 

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

1322 

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

1324 empty list) or composite glyphs. 

1325 """ 

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

1327 if self.isComposite(): 

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 else: 

1371 self.program = ttProgram.Program() 

1372 self.program.fromBytecode([]) 

1373 # No padding to trim. 

1374 return 

1375 if not self.data: 

1376 return 

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

1378 data = bytearray(self.data) 

1379 i = 10 

1380 if numContours >= 0: 

1381 i += 2 * numContours # endPtsOfContours 

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

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

1384 if remove_hinting: 

1385 # Zero instruction length 

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

1387 i += 2 

1388 if instructionLen: 

1389 # Splice it out 

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

1391 instructionLen = 0 

1392 else: 

1393 i += 2 + instructionLen 

1394 

1395 coordBytes = 0 

1396 j = 0 

1397 while True: 

1398 flag = data[i] 

1399 i = i + 1 

1400 repeat = 1 

1401 if flag & flagRepeat: 

1402 repeat = data[i] + 1 

1403 i = i + 1 

1404 xBytes = yBytes = 0 

1405 if flag & flagXShort: 

1406 xBytes = 1 

1407 elif not (flag & flagXsame): 

1408 xBytes = 2 

1409 if flag & flagYShort: 

1410 yBytes = 1 

1411 elif not (flag & flagYsame): 

1412 yBytes = 2 

1413 coordBytes += (xBytes + yBytes) * repeat 

1414 j += repeat 

1415 if j >= nCoordinates: 

1416 break 

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

1418 i += coordBytes 

1419 # Remove padding 

1420 data = data[:i] 

1421 elif self.isComposite(): 

1422 more = 1 

1423 we_have_instructions = False 

1424 while more: 

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

1426 if remove_hinting: 

1427 flags &= ~WE_HAVE_INSTRUCTIONS 

1428 if flags & WE_HAVE_INSTRUCTIONS: 

1429 we_have_instructions = True 

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

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

1432 i += 4 

1433 flags = int(flags) 

1434 

1435 if flags & ARG_1_AND_2_ARE_WORDS: 

1436 i += 4 

1437 else: 

1438 i += 2 

1439 if flags & WE_HAVE_A_SCALE: 

1440 i += 2 

1441 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1442 i += 4 

1443 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1444 i += 8 

1445 more = flags & MORE_COMPONENTS 

1446 if we_have_instructions: 

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

1448 i += 2 + instructionLen 

1449 # Remove padding 

1450 data = data[:i] 

1451 

1452 self.data = data 

1453 

1454 def removeHinting(self): 

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

1456 self.trim(remove_hinting=True) 

1457 

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

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

1460 

1461 Arguments: 

1462 pen: An object conforming to the pen protocol. 

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

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

1465 translated by this offset. 

1466 """ 

1467 

1468 if self.isComposite(): 

1469 for component in self.components: 

1470 glyphName, transform = component.getComponentInfo() 

1471 pen.addComponent(glyphName, transform) 

1472 return 

1473 

1474 self.expand(glyfTable) 

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

1476 if offset: 

1477 coordinates = coordinates.copy() 

1478 coordinates.translate((offset, 0)) 

1479 start = 0 

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

1481 for end in endPts: 

1482 end = end + 1 

1483 contour = coordinates[start:end] 

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

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

1486 start = end 

1487 if 1 not in cFlags: 

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

1489 cubic = all(cuFlags) 

1490 if cubic: 

1491 count = len(contour) 

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

1493 l = contour[-1] 

1494 f = contour[0] 

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

1496 pen.moveTo(p0) 

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

1498 p1 = contour[i] 

1499 p2 = contour[i + 1] 

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

1501 p3 = ( 

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

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

1504 ) 

1505 pen.curveTo(p1, p2, p3) 

1506 else: 

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

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

1509 # as the on-curve point. 

1510 contour.append(None) 

1511 pen.qCurveTo(*contour) 

1512 else: 

1513 # Shuffle the points so that the contour is guaranteed 

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

1515 # the moveTo. 

1516 firstOnCurve = cFlags.index(1) + 1 

1517 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 

1518 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 

1519 cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve] 

1520 pen.moveTo(contour[-1]) 

1521 while contour: 

1522 nextOnCurve = cFlags.index(1) + 1 

1523 if nextOnCurve == 1: 

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

1525 # pen.closePath() 

1526 if len(contour) > 1: 

1527 pen.lineTo(contour[0]) 

1528 else: 

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

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

1531 cubic = any(cubicFlags) 

1532 if cubic: 

1533 assert all( 

1534 cubicFlags 

1535 ), "Mixed cubic and quadratic segment undefined" 

1536 

1537 count = nextOnCurve 

1538 assert ( 

1539 count >= 3 

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

1541 assert ( 

1542 count - 1 

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

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

1545 p1 = contour[i] 

1546 p2 = contour[i + 1] 

1547 p4 = contour[i + 2] 

1548 p3 = ( 

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

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

1551 ) 

1552 lastOnCurve = p3 

1553 pen.curveTo(p1, p2, p3) 

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

1555 else: 

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

1557 contour = contour[nextOnCurve:] 

1558 cFlags = cFlags[nextOnCurve:] 

1559 cuFlags = cuFlags[nextOnCurve:] 

1560 pen.closePath() 

1561 

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

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

1564 this will not change the point indices. 

1565 """ 

1566 

1567 if self.isComposite(): 

1568 for component in self.components: 

1569 glyphName, transform = component.getComponentInfo() 

1570 pen.addComponent(glyphName, transform) 

1571 return 

1572 

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

1574 if offset: 

1575 coordinates = coordinates.copy() 

1576 coordinates.translate((offset, 0)) 

1577 start = 0 

1578 for end in endPts: 

1579 end = end + 1 

1580 contour = coordinates[start:end] 

1581 cFlags = flags[start:end] 

1582 start = end 

1583 pen.beginPath() 

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

1585 

1586 if cFlags[-1] & flagOnCurve: 

1587 segmentType = "line" 

1588 elif cFlags[-1] & flagCubic: 

1589 segmentType = "curve" 

1590 else: 

1591 segmentType = "qcurve" 

1592 for i, pt in enumerate(contour): 

1593 if cFlags[i] & flagOnCurve: 

1594 pen.addPoint(pt, segmentType=segmentType) 

1595 segmentType = "line" 

1596 else: 

1597 pen.addPoint(pt) 

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

1599 pen.endPath() 

1600 

1601 def __eq__(self, other): 

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

1603 return NotImplemented 

1604 return self.__dict__ == other.__dict__ 

1605 

1606 def __ne__(self, other): 

1607 result = self.__eq__(other) 

1608 return result if result is NotImplemented else not result 

1609 

1610 

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

1612# to use otRound below 

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

1614 

1615 

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

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

1618 p0 = Vector(p0) 

1619 p1 = Vector(p1) 

1620 p2 = Vector(p2) 

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

1622 

1623 

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

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

1626 

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

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

1629 

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

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

1632 for all of them will actually be implied. 

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

1634 contours are considered. 

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

1636 

1637 Args: 

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

1639 

1640 Returns: 

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

1642 

1643 Raises: 

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

1645 different point flags or number of contours. 

1646 

1647 Reference: 

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

1649 """ 

1650 staticAttributes = SimpleNamespace( 

1651 numberOfContours=None, flags=None, endPtsOfContours=None 

1652 ) 

1653 drop = None 

1654 simple_glyphs = [] 

1655 for i, glyph in enumerate(interpolatable_glyphs): 

1656 if glyph.numberOfContours < 1: 

1657 # ignore composite or empty glyphs 

1658 continue 

1659 

1660 for attr in staticAttributes.__dict__: 

1661 expected = getattr(staticAttributes, attr) 

1662 found = getattr(glyph, attr) 

1663 if expected is None: 

1664 setattr(staticAttributes, attr, found) 

1665 elif expected != found: 

1666 raise ValueError( 

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

1668 f"expected {expected}, found {found}" 

1669 ) 

1670 

1671 may_drop = set() 

1672 start = 0 

1673 coords = glyph.coordinates 

1674 flags = staticAttributes.flags 

1675 endPtsOfContours = staticAttributes.endPtsOfContours 

1676 for last in endPtsOfContours: 

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

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

1679 continue 

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

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

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

1683 continue 

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

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

1686 continue 

1687 

1688 may_drop.add(i) 

1689 start = last + 1 

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

1691 if drop is None: 

1692 drop = may_drop 

1693 else: 

1694 drop.intersection_update(may_drop) 

1695 

1696 simple_glyphs.append(glyph) 

1697 

1698 if drop: 

1699 # Do the actual dropping 

1700 flags = staticAttributes.flags 

1701 assert flags is not None 

1702 newFlags = array.array( 

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

1704 ) 

1705 

1706 endPts = staticAttributes.endPtsOfContours 

1707 assert endPts is not None 

1708 newEndPts = [] 

1709 i = 0 

1710 delta = 0 

1711 for d in sorted(drop): 

1712 while d > endPts[i]: 

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

1714 i += 1 

1715 delta += 1 

1716 while i < len(endPts): 

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

1718 i += 1 

1719 

1720 for glyph in simple_glyphs: 

1721 coords = glyph.coordinates 

1722 glyph.coordinates = GlyphCoordinates( 

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

1724 ) 

1725 glyph.flags = newFlags 

1726 glyph.endPtsOfContours = newEndPts 

1727 

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

1729 

1730 

1731class GlyphComponent(object): 

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

1733 

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

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

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

1737 attribute is not present. 

1738 """ 

1739 

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

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

1742 # mind - see below. 

1743 

1744 def __init__(self): 

1745 pass 

1746 

1747 def getComponentInfo(self): 

1748 """Return information about the component 

1749 

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

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

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

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

1754 matrix is not present. 

1755 """ 

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

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

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

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

1760 # this TT feature. 

1761 if hasattr(self, "transform"): 

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

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

1764 else: 

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

1766 return self.glyphName, trans 

1767 

1768 def decompile(self, data, glyfTable): 

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

1770 self.flags = int(flags) 

1771 glyphID = int(glyphID) 

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

1773 data = data[4:] 

1774 

1775 if self.flags & ARG_1_AND_2_ARE_WORDS: 

1776 if self.flags & ARGS_ARE_XY_VALUES: 

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

1778 else: 

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

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

1781 data = data[4:] 

1782 else: 

1783 if self.flags & ARGS_ARE_XY_VALUES: 

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

1785 else: 

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

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

1788 data = data[2:] 

1789 

1790 if self.flags & WE_HAVE_A_SCALE: 

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

1792 self.transform = [ 

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

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

1795 ] # fixed 2.14 

1796 data = data[2:] 

1797 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: 

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

1799 self.transform = [ 

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

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

1802 ] # fixed 2.14 

1803 data = data[4:] 

1804 elif self.flags & WE_HAVE_A_TWO_BY_TWO: 

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

1806 self.transform = [ 

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

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

1809 ] # fixed 2.14 

1810 data = data[8:] 

1811 more = self.flags & MORE_COMPONENTS 

1812 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS 

1813 self.flags = self.flags & ( 

1814 ROUND_XY_TO_GRID 

1815 | USE_MY_METRICS 

1816 | SCALED_COMPONENT_OFFSET 

1817 | UNSCALED_COMPONENT_OFFSET 

1818 | NON_OVERLAPPING 

1819 | OVERLAP_COMPOUND 

1820 ) 

1821 return more, haveInstructions, data 

1822 

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

1824 data = b"" 

1825 

1826 # reset all flags we will calculate ourselves 

1827 flags = self.flags & ( 

1828 ROUND_XY_TO_GRID 

1829 | USE_MY_METRICS 

1830 | SCALED_COMPONENT_OFFSET 

1831 | UNSCALED_COMPONENT_OFFSET 

1832 | NON_OVERLAPPING 

1833 | OVERLAP_COMPOUND 

1834 ) 

1835 if more: 

1836 flags = flags | MORE_COMPONENTS 

1837 if haveInstructions: 

1838 flags = flags | WE_HAVE_INSTRUCTIONS 

1839 

1840 if hasattr(self, "firstPt"): 

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

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

1843 else: 

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

1845 flags = flags | ARG_1_AND_2_ARE_WORDS 

1846 else: 

1847 x = otRound(self.x) 

1848 y = otRound(self.y) 

1849 flags = flags | ARGS_ARE_XY_VALUES 

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

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

1852 else: 

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

1854 flags = flags | ARG_1_AND_2_ARE_WORDS 

1855 

1856 if hasattr(self, "transform"): 

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

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

1859 flags = flags | WE_HAVE_A_TWO_BY_TWO 

1860 data = data + struct.pack( 

1861 ">hhhh", 

1862 transform[0][0], 

1863 transform[0][1], 

1864 transform[1][0], 

1865 transform[1][1], 

1866 ) 

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

1868 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE 

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

1870 else: 

1871 flags = flags | WE_HAVE_A_SCALE 

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

1873 

1874 glyphID = glyfTable.getGlyphID(self.glyphName) 

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

1876 

1877 def toXML(self, writer, ttFont): 

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

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

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

1881 else: 

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

1883 

1884 if hasattr(self, "transform"): 

1885 transform = self.transform 

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

1887 attrs = attrs + [ 

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

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

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

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

1892 ] 

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

1894 attrs = attrs + [ 

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

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

1897 ] 

1898 else: 

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

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

1901 writer.simpletag("component", attrs) 

1902 writer.newline() 

1903 

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

1905 self.glyphName = attrs["glyphName"] 

1906 if "firstPt" in attrs: 

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

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

1909 else: 

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

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

1912 if "scale01" in attrs: 

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

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

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

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

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

1918 elif "scalex" in attrs: 

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

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

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

1922 elif "scale" in attrs: 

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

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

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

1926 

1927 def __eq__(self, other): 

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

1929 return NotImplemented 

1930 return self.__dict__ == other.__dict__ 

1931 

1932 def __ne__(self, other): 

1933 result = self.__eq__(other) 

1934 return result if result is NotImplemented else not result 

1935 

1936 def _hasOnlyIntegerTranslate(self): 

1937 """Return True if it's a 'simple' component. 

1938 

1939 That is, it has no anchor points and no transform other than integer translate. 

1940 """ 

1941 return ( 

1942 not hasattr(self, "firstPt") 

1943 and not hasattr(self, "transform") 

1944 and float(self.x).is_integer() 

1945 and float(self.y).is_integer() 

1946 ) 

1947 

1948 

1949class GlyphCoordinates(object): 

1950 """A list of glyph coordinates. 

1951 

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

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

1954 """ 

1955 

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

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

1958 self.extend(iterable) 

1959 

1960 @property 

1961 def array(self): 

1962 """Returns the underlying array of coordinates""" 

1963 return self._a 

1964 

1965 @staticmethod 

1966 def zeros(count): 

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

1968 g = GlyphCoordinates() 

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

1970 return g 

1971 

1972 def copy(self): 

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

1974 c = GlyphCoordinates() 

1975 c._a.extend(self._a) 

1976 return c 

1977 

1978 def __len__(self): 

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

1980 return len(self._a) // 2 

1981 

1982 def __getitem__(self, k): 

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

1984 a = self._a 

1985 if isinstance(k, slice): 

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

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

1988 ret = [] 

1989 for k in indices: 

1990 x = a[2 * k] 

1991 y = a[2 * k + 1] 

1992 ret.append( 

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

1994 ) 

1995 return ret 

1996 x = a[2 * k] 

1997 y = a[2 * k + 1] 

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

1999 

2000 def __setitem__(self, k, v): 

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

2002 if isinstance(k, slice): 

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

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

2005 for j, i in enumerate(indices): 

2006 self[i] = v[j] 

2007 return 

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

2009 

2010 def __delitem__(self, i): 

2011 """Removes a point from the list""" 

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

2013 del self._a[i] 

2014 del self._a[i] 

2015 

2016 def __repr__(self): 

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

2018 

2019 def append(self, p): 

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

2021 

2022 def extend(self, iterable): 

2023 for p in iterable: 

2024 self._a.extend(p) 

2025 

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

2027 if round is noRound: 

2028 return 

2029 a = self._a 

2030 for i, value in enumerate(a): 

2031 a[i] = round(value) 

2032 

2033 def calcBounds(self): 

2034 a = self._a 

2035 if not a: 

2036 return 0, 0, 0, 0 

2037 xs = a[0::2] 

2038 ys = a[1::2] 

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

2040 

2041 def calcIntBounds(self, round=otRound): 

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

2043 

2044 def relativeToAbsolute(self): 

2045 a = self._a 

2046 x, y = 0, 0 

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

2048 a[i] = x = a[i] + x 

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

2050 

2051 def absoluteToRelative(self): 

2052 a = self._a 

2053 x, y = 0, 0 

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

2055 nx = a[i] 

2056 ny = a[i + 1] 

2057 a[i] = nx - x 

2058 a[i + 1] = ny - y 

2059 x = nx 

2060 y = ny 

2061 

2062 def translate(self, p): 

2063 """ 

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

2065 """ 

2066 x, y = p 

2067 if x == 0 and y == 0: 

2068 return 

2069 a = self._a 

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

2071 a[i] += x 

2072 a[i + 1] += y 

2073 

2074 def scale(self, p): 

2075 """ 

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

2077 """ 

2078 x, y = p 

2079 if x == 1 and y == 1: 

2080 return 

2081 a = self._a 

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

2083 a[i] *= x 

2084 a[i + 1] *= y 

2085 

2086 def transform(self, t): 

2087 """ 

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

2089 """ 

2090 a = self._a 

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

2092 x = a[i] 

2093 y = a[i + 1] 

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

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

2096 a[i] = px 

2097 a[i + 1] = py 

2098 

2099 def __eq__(self, other): 

2100 """ 

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

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

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

2104 >>> g == g2 

2105 True 

2106 >>> g == g3 

2107 False 

2108 >>> g2 == g3 

2109 False 

2110 """ 

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

2112 return NotImplemented 

2113 return self._a == other._a 

2114 

2115 def __ne__(self, other): 

2116 """ 

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

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

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

2120 >>> g != g2 

2121 False 

2122 >>> g != g3 

2123 True 

2124 >>> g2 != g3 

2125 True 

2126 """ 

2127 result = self.__eq__(other) 

2128 return result if result is NotImplemented else not result 

2129 

2130 # Math operations 

2131 

2132 def __pos__(self): 

2133 """ 

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

2135 >>> g 

2136 GlyphCoordinates([(1, 2)]) 

2137 >>> g2 = +g 

2138 >>> g2 

2139 GlyphCoordinates([(1, 2)]) 

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

2141 >>> g2 

2142 GlyphCoordinates([(2, 2)]) 

2143 >>> g 

2144 GlyphCoordinates([(1, 2)]) 

2145 """ 

2146 return self.copy() 

2147 

2148 def __neg__(self): 

2149 """ 

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

2151 >>> g 

2152 GlyphCoordinates([(1, 2)]) 

2153 >>> g2 = -g 

2154 >>> g2 

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

2156 >>> g 

2157 GlyphCoordinates([(1, 2)]) 

2158 """ 

2159 r = self.copy() 

2160 a = r._a 

2161 for i, value in enumerate(a): 

2162 a[i] = -value 

2163 return r 

2164 

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

2166 r = self.copy() 

2167 r.toInt(round=round) 

2168 return r 

2169 

2170 def __add__(self, other): 

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

2172 

2173 def __sub__(self, other): 

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

2175 

2176 def __mul__(self, other): 

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

2178 

2179 def __truediv__(self, other): 

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

2181 

2182 __radd__ = __add__ 

2183 __rmul__ = __mul__ 

2184 

2185 def __rsub__(self, other): 

2186 return other + (-self) 

2187 

2188 def __iadd__(self, other): 

2189 """ 

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

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

2192 >>> g 

2193 GlyphCoordinates([(1.5, 2)]) 

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

2195 >>> g += g2 

2196 >>> g 

2197 GlyphCoordinates([(4.5, 6)]) 

2198 """ 

2199 if isinstance(other, tuple): 

2200 assert len(other) == 2 

2201 self.translate(other) 

2202 return self 

2203 if isinstance(other, GlyphCoordinates): 

2204 other = other._a 

2205 a = self._a 

2206 assert len(a) == len(other) 

2207 for i, value in enumerate(other): 

2208 a[i] += value 

2209 return self 

2210 return NotImplemented 

2211 

2212 def __isub__(self, other): 

2213 """ 

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

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

2216 >>> g 

2217 GlyphCoordinates([(0.5, 2)]) 

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

2219 >>> g -= g2 

2220 >>> g 

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

2222 """ 

2223 if isinstance(other, tuple): 

2224 assert len(other) == 2 

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

2226 return self 

2227 if isinstance(other, GlyphCoordinates): 

2228 other = other._a 

2229 a = self._a 

2230 assert len(a) == len(other) 

2231 for i, value in enumerate(other): 

2232 a[i] -= value 

2233 return self 

2234 return NotImplemented 

2235 

2236 def __imul__(self, other): 

2237 """ 

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

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

2240 >>> g *= 2 

2241 >>> g 

2242 GlyphCoordinates([(4, 2)]) 

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

2244 >>> g *= 2 

2245 >>> g 

2246 GlyphCoordinates([(2, 4)]) 

2247 """ 

2248 if isinstance(other, tuple): 

2249 assert len(other) == 2 

2250 self.scale(other) 

2251 return self 

2252 if isinstance(other, Number): 

2253 if other == 1: 

2254 return self 

2255 a = self._a 

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

2257 a[i] *= other 

2258 return self 

2259 return NotImplemented 

2260 

2261 def __itruediv__(self, other): 

2262 """ 

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

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

2265 >>> g /= 2 

2266 >>> g 

2267 GlyphCoordinates([(1, 1)]) 

2268 """ 

2269 if isinstance(other, Number): 

2270 other = (other, other) 

2271 if isinstance(other, tuple): 

2272 if other == (1, 1): 

2273 return self 

2274 assert len(other) == 2 

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

2276 return self 

2277 return NotImplemented 

2278 

2279 def __bool__(self): 

2280 """ 

2281 >>> g = GlyphCoordinates([]) 

2282 >>> bool(g) 

2283 False 

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

2285 >>> bool(g) 

2286 True 

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

2288 >>> bool(g) 

2289 True 

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

2291 >>> bool(g) 

2292 True 

2293 """ 

2294 return bool(self._a) 

2295 

2296 __nonzero__ = __bool__ 

2297 

2298 

2299if __name__ == "__main__": 

2300 import doctest, sys 

2301 

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