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

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

1399 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 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 optimizeSpeed = ttFont.cfg[ttLib.OPTIMIZE_FONT_SPEED] 

138 

139 self.axisTags = ( 

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

141 ) 

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

143 self.glyphOrder = ttFont.getGlyphOrder() 

144 padding = self.padding 

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

146 locations = [] 

147 currentLocation = 0 

148 dataList = [] 

149 recalcBBoxes = ttFont.recalcBBoxes 

150 boundsDone = set() 

151 for glyphName in self.glyphOrder: 

152 glyph = self.glyphs[glyphName] 

153 glyphData = glyph.compile( 

154 self, 

155 recalcBBoxes, 

156 boundsDone=boundsDone, 

157 optimizeSize=not optimizeSpeed, 

158 ) 

159 if padding > 1: 

160 glyphData = pad(glyphData, size=padding) 

161 locations.append(currentLocation) 

162 currentLocation = currentLocation + len(glyphData) 

163 dataList.append(glyphData) 

164 locations.append(currentLocation) 

165 

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

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

168 # table to use the short offsets. 

169 indices = [ 

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

171 ] 

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

173 # It fits. Do it. 

174 for i in indices: 

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

176 currentLocation = 0 

177 for i, glyphData in enumerate(dataList): 

178 locations[i] = currentLocation 

179 currentLocation += len(glyphData) 

180 locations[len(dataList)] = currentLocation 

181 

182 data = b"".join(dataList) 

183 if "loca" in ttFont: 

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

185 if "maxp" in ttFont: 

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

187 if not data: 

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

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

190 # on Windows as well. 

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

192 data = b"\0" 

193 return data 

194 

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

196 notice = ( 

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

198 "will be recalculated by the compiler." 

199 ) 

200 glyphNames = ttFont.getGlyphNames() 

201 if not splitGlyphs: 

202 writer.newline() 

203 writer.comment(notice) 

204 writer.newline() 

205 writer.newline() 

206 numGlyphs = len(glyphNames) 

207 if splitGlyphs: 

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

209 existingGlyphFiles = set() 

210 for glyphName in glyphNames: 

211 glyph = self.get(glyphName) 

212 if glyph is None: 

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

214 continue 

215 if glyph.numberOfContours: 

216 if splitGlyphs: 

217 glyphPath = userNameToFileName( 

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

219 existingGlyphFiles, 

220 prefix=path + ".", 

221 suffix=ext, 

222 ) 

223 existingGlyphFiles.add(glyphPath.lower()) 

224 glyphWriter = xmlWriter.XMLWriter( 

225 glyphPath, 

226 idlefunc=writer.idlefunc, 

227 newlinestr=writer.newlinestr, 

228 ) 

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

230 glyphWriter.newline() 

231 glyphWriter.begintag("glyf") 

232 glyphWriter.newline() 

233 glyphWriter.comment(notice) 

234 glyphWriter.newline() 

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

236 else: 

237 glyphWriter = writer 

238 glyphWriter.begintag( 

239 "TTGlyph", 

240 [ 

241 ("name", glyphName), 

242 ("xMin", glyph.xMin), 

243 ("yMin", glyph.yMin), 

244 ("xMax", glyph.xMax), 

245 ("yMax", glyph.yMax), 

246 ], 

247 ) 

248 glyphWriter.newline() 

249 glyph.toXML(glyphWriter, ttFont) 

250 glyphWriter.endtag("TTGlyph") 

251 glyphWriter.newline() 

252 if splitGlyphs: 

253 glyphWriter.endtag("glyf") 

254 glyphWriter.newline() 

255 glyphWriter.endtag("ttFont") 

256 glyphWriter.newline() 

257 glyphWriter.close() 

258 else: 

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

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

261 if not splitGlyphs: 

262 writer.newline() 

263 writer.newline() 

264 

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

266 if name != "TTGlyph": 

267 return 

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

269 self.glyphs = {} 

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

271 self.glyphOrder = ttFont.getGlyphOrder() 

272 glyphName = attrs["name"] 

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

274 glyph = Glyph() 

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

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

277 self.glyphs[glyphName] = glyph 

278 for element in content: 

279 if not isinstance(element, tuple): 

280 continue 

281 name, attrs, content = element 

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

283 if not ttFont.recalcBBoxes: 

284 glyph.compact(self, 0) 

285 

286 def setGlyphOrder(self, glyphOrder): 

287 """Sets the glyph order 

288 

289 Args: 

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

291 """ 

292 self.glyphOrder = glyphOrder 

293 self._reverseGlyphOrder = {} 

294 

295 def getGlyphName(self, glyphID): 

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

297 

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

299 """ 

300 return self.glyphOrder[glyphID] 

301 

302 def _buildReverseGlyphOrderDict(self): 

303 self._reverseGlyphOrder = d = {} 

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

305 d[glyphName] = glyphID 

306 

307 def getGlyphID(self, glyphName): 

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

309 

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

311 """ 

312 glyphOrder = self.glyphOrder 

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

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

315 self._buildReverseGlyphOrderDict() 

316 id = self._reverseGlyphOrder.get(glyphName) 

317 if id is None: 

318 raise ValueError(glyphName) 

319 return id 

320 

321 def removeHinting(self): 

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

323 

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

325 """ 

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

327 glyph.removeHinting() 

328 

329 def keys(self): 

330 return self.glyphs.keys() 

331 

332 def has_key(self, glyphName): 

333 return glyphName in self.glyphs 

334 

335 __contains__ = has_key 

336 

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

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

339 if glyph is not None: 

340 glyph.expand(self) 

341 return glyph 

342 

343 def __getitem__(self, glyphName): 

344 glyph = self.glyphs[glyphName] 

345 glyph.expand(self) 

346 return glyph 

347 

348 def __setitem__(self, glyphName, glyph): 

349 self.glyphs[glyphName] = glyph 

350 if glyphName not in self.glyphOrder: 

351 self.glyphOrder.append(glyphName) 

352 

353 def __delitem__(self, glyphName): 

354 del self.glyphs[glyphName] 

355 self.glyphOrder.remove(glyphName) 

356 

357 def __len__(self): 

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

359 return len(self.glyphs) 

360 

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

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

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

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

365 

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

367 

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

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

370 

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

372 """ 

373 glyph = self[glyphName] 

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

375 glyph.recalcBounds(self) 

376 

377 horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName] 

378 leftSideX = glyph.xMin - leftSideBearing 

379 rightSideX = leftSideX + horizontalAdvanceWidth 

380 

381 if vMetrics: 

382 verticalAdvanceWidth, topSideBearing = vMetrics[glyphName] 

383 topSideY = topSideBearing + glyph.yMax 

384 bottomSideY = topSideY - verticalAdvanceWidth 

385 else: 

386 bottomSideY = topSideY = 0 

387 

388 return [ 

389 (leftSideX, 0), 

390 (rightSideX, 0), 

391 (0, topSideY), 

392 (0, bottomSideY), 

393 ] 

394 

395 def _getCoordinatesAndControls( 

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

397 ): 

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

399 

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

401 as mandated by the "gvar" spec. 

402 

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

404 - numberOfContours: -1 for composite glyphs. 

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

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

407 optimization). 

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

409 composite glyphs). 

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

411 composite glyphs (None for simple glyphs). 

412 

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

414 the "_getPhantomPoints" method). 

415 

416 Return None if the requested glyphName is not present. 

417 """ 

418 glyph = self.get(glyphName) 

419 if glyph is None: 

420 return None 

421 if glyph.isComposite(): 

422 coords = GlyphCoordinates( 

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

424 ) 

425 controls = _GlyphControls( 

426 numberOfContours=glyph.numberOfContours, 

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

428 flags=None, 

429 components=[ 

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

431 for c in glyph.components 

432 ], 

433 ) 

434 else: 

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

436 coords = coords.copy() 

437 controls = _GlyphControls( 

438 numberOfContours=glyph.numberOfContours, 

439 endPts=endPts, 

440 flags=flags, 

441 components=None, 

442 ) 

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

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

445 coords.extend(phantomPoints) 

446 coords.toInt(round=round) 

447 return coords, controls 

448 

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

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

451 

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

453 points" as the last four coordinates. 

454 

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

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

457 the glyph's bounding boxes. 

458 

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

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

461 method). 

462 """ 

463 glyph = self[glyphName] 

464 

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

466 assert len(coord) >= 4 

467 leftSideX = coord[-4][0] 

468 rightSideX = coord[-3][0] 

469 topSideY = coord[-2][1] 

470 bottomSideY = coord[-1][1] 

471 

472 coord = coord[:-4] 

473 

474 if glyph.isComposite(): 

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

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

477 if hasattr(comp, "x"): 

478 comp.x, comp.y = p 

479 elif glyph.numberOfContours == 0: 

480 assert len(coord) == 0 

481 else: 

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

483 glyph.coordinates = GlyphCoordinates(coord) 

484 

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

486 

487 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 

488 if horizontalAdvanceWidth < 0: 

489 # unlikely, but it can happen, see: 

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

491 horizontalAdvanceWidth = 0 

492 leftSideBearing = otRound(glyph.xMin - leftSideX) 

493 hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing 

494 

495 if vMetrics is not None: 

496 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 

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

498 verticalAdvanceWidth = 0 

499 topSideBearing = otRound(topSideY - glyph.yMax) 

500 vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing 

501 

502 # Deprecated 

503 

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

505 """This method is wrong and deprecated. 

506 For rationale see: 

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

508 """ 

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

510 if vMetrics is None: 

511 verticalAdvanceWidth = ttFont["head"].unitsPerEm 

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

513 if topSideY is None: 

514 if defaultVerticalOrigin is not None: 

515 topSideY = defaultVerticalOrigin 

516 else: 

517 topSideY = verticalAdvanceWidth 

518 glyph = self[glyphName] 

519 glyph.recalcBounds(self) 

520 topSideBearing = otRound(topSideY - glyph.yMax) 

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

522 return vMetrics 

523 

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

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

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

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

528 hMetrics = ttFont["hmtx"].metrics 

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

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

531 

532 @deprecateFunction( 

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

534 ) 

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

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

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

538 hMetrics = ttFont["hmtx"].metrics 

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

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

541 

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

543 def setCoordinates(self, glyphName, ttFont): 

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

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

546 hMetrics = ttFont["hmtx"].metrics 

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

548 self._setCoordinates(glyphName, hMetrics, vMetrics) 

549 

550 

551_GlyphControls = namedtuple( 

552 "_GlyphControls", "numberOfContours endPts flags components" 

553) 

554 

555 

556glyphHeaderFormat = """ 

557 > # big endian 

558 numberOfContours: h 

559 xMin: h 

560 yMin: h 

561 xMax: h 

562 yMax: h 

563""" 

564 

565# flags 

566flagOnCurve = 0x01 

567flagXShort = 0x02 

568flagYShort = 0x04 

569flagRepeat = 0x08 

570flagXsame = 0x10 

571flagYsame = 0x20 

572flagOverlapSimple = 0x40 

573flagCubic = 0x80 

574 

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

576keepFlags = flagOnCurve + flagOverlapSimple + flagCubic 

577 

578_flagSignBytes = { 

579 0: 2, 

580 flagXsame: 0, 

581 flagXShort | flagXsame: +1, 

582 flagXShort: -1, 

583 flagYsame: 0, 

584 flagYShort | flagYsame: +1, 

585 flagYShort: -1, 

586} 

587 

588 

589def flagBest(x, y, onCurve): 

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

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

592 

593 flag = flagOnCurve if onCurve else 0 

594 cost = 0 

595 # do x 

596 if x == 0: 

597 flag = flag | flagXsame 

598 elif -255 <= x <= 255: 

599 flag = flag | flagXShort 

600 if x > 0: 

601 flag = flag | flagXsame 

602 cost += 1 

603 else: 

604 cost += 2 

605 # do y 

606 if y == 0: 

607 flag = flag | flagYsame 

608 elif -255 <= y <= 255: 

609 flag = flag | flagYShort 

610 if y > 0: 

611 flag = flag | flagYsame 

612 cost += 1 

613 else: 

614 cost += 2 

615 return flag, cost 

616 

617 

618def flagFits(newFlag, oldFlag, mask): 

619 newBytes = _flagSignBytes[newFlag & mask] 

620 oldBytes = _flagSignBytes[oldFlag & mask] 

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

622 

623 

624def flagSupports(newFlag, oldFlag): 

625 return ( 

626 (oldFlag & flagOnCurve) == (newFlag & flagOnCurve) 

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

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

629 ) 

630 

631 

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

633 byteCount = _flagSignBytes[flag & mask] 

634 if byteCount == 1: 

635 coordBytes.append(coord) 

636 elif byteCount == -1: 

637 coordBytes.append(-coord) 

638 elif byteCount == 2: 

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

640 

641 

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

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

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

645 

646 

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

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

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

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

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

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

653WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy 

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

655WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow 

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

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

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

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

660 

661 

662CompositeMaxpValues = namedtuple( 

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

664) 

665 

666 

667class Glyph(object): 

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

669 

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

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

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

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

674 attributes. 

675 

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

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

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

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

680 access its data. 

681 

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

683 

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

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

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

687 

688 """ 

689 

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

691 if not data: 

692 # empty char 

693 self.numberOfContours = 0 

694 return 

695 self.data = data 

696 

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

698 data = self.compile(glyfTable, recalcBBoxes) 

699 self.__dict__.clear() 

700 self.data = data 

701 

702 def expand(self, glyfTable): 

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

704 # already unpacked 

705 return 

706 if not self.data: 

707 # empty char 

708 del self.data 

709 self.numberOfContours = 0 

710 return 

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

712 del self.data 

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

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

715 # one, so short-circuit here. 

716 if self.numberOfContours == 0: 

717 return 

718 if self.isComposite(): 

719 self.decompileComponents(data, glyfTable) 

720 else: 

721 self.decompileCoordinates(data) 

722 

723 def compile( 

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

725 ): 

726 if hasattr(self, "data"): 

727 if recalcBBoxes: 

728 # must unpack glyph in order to recalculate bounding box 

729 self.expand(glyfTable) 

730 else: 

731 return self.data 

732 if self.numberOfContours == 0: 

733 return b"" 

734 

735 if recalcBBoxes: 

736 self.recalcBounds(glyfTable, boundsDone=boundsDone) 

737 

738 data = sstruct.pack(glyphHeaderFormat, self) 

739 if self.isComposite(): 

740 data = data + self.compileComponents(glyfTable) 

741 else: 

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

743 return data 

744 

745 def toXML(self, writer, ttFont): 

746 if self.isComposite(): 

747 for compo in self.components: 

748 compo.toXML(writer, ttFont) 

749 haveInstructions = hasattr(self, "program") 

750 else: 

751 last = 0 

752 for i in range(self.numberOfContours): 

753 writer.begintag("contour") 

754 writer.newline() 

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

756 attrs = [ 

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

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

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

760 ] 

761 if self.flags[j] & flagOverlapSimple: 

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

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

764 if self.flags[j] & flagCubic: 

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

766 writer.simpletag("pt", attrs) 

767 writer.newline() 

768 last = self.endPtsOfContours[i] + 1 

769 writer.endtag("contour") 

770 writer.newline() 

771 haveInstructions = self.numberOfContours > 0 

772 if haveInstructions: 

773 if self.program: 

774 writer.begintag("instructions") 

775 writer.newline() 

776 self.program.toXML(writer, ttFont) 

777 writer.endtag("instructions") 

778 else: 

779 writer.simpletag("instructions") 

780 writer.newline() 

781 

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

783 if name == "contour": 

784 if self.numberOfContours < 0: 

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

786 self.numberOfContours = self.numberOfContours + 1 

787 coordinates = GlyphCoordinates() 

788 flags = bytearray() 

789 for element in content: 

790 if not isinstance(element, tuple): 

791 continue 

792 name, attrs, content = element 

793 if name != "pt": 

794 continue # ignore anything but "pt" 

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

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

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

798 flag |= flagOverlapSimple 

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

800 flag |= flagCubic 

801 flags.append(flag) 

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

803 self.coordinates = coordinates 

804 self.flags = flags 

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

806 else: 

807 self.coordinates.extend(coordinates) 

808 self.flags.extend(flags) 

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

810 elif name == "component": 

811 if self.numberOfContours > 0: 

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

813 self.numberOfContours = -1 

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

815 self.components = [] 

816 component = GlyphComponent() 

817 self.components.append(component) 

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

819 elif name == "instructions": 

820 self.program = ttProgram.Program() 

821 for element in content: 

822 if not isinstance(element, tuple): 

823 continue 

824 name, attrs, content = element 

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

826 

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

828 assert self.isComposite() 

829 nContours = 0 

830 nPoints = 0 

831 initialMaxComponentDepth = maxComponentDepth 

832 for compo in self.components: 

833 baseGlyph = glyfTable[compo.glyphName] 

834 if baseGlyph.numberOfContours == 0: 

835 continue 

836 elif baseGlyph.numberOfContours > 0: 

837 nP, nC = baseGlyph.getMaxpValues() 

838 else: 

839 nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues( 

840 glyfTable, initialMaxComponentDepth + 1 

841 ) 

842 maxComponentDepth = max(maxComponentDepth, componentDepth) 

843 nPoints = nPoints + nP 

844 nContours = nContours + nC 

845 return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) 

846 

847 def getMaxpValues(self): 

848 assert self.numberOfContours > 0 

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

850 

851 def decompileComponents(self, data, glyfTable): 

852 self.components = [] 

853 more = 1 

854 haveInstructions = 0 

855 while more: 

856 component = GlyphComponent() 

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

858 haveInstructions = haveInstructions | haveInstr 

859 self.components.append(component) 

860 if haveInstructions: 

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

862 data = data[2:] 

863 self.program = ttProgram.Program() 

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

865 data = data[numInstructions:] 

866 if len(data) >= 4: 

867 log.warning( 

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

869 len(data), 

870 ) 

871 

872 def decompileCoordinates(self, data): 

873 endPtsOfContours = array.array("H") 

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

875 if sys.byteorder != "big": 

876 endPtsOfContours.byteswap() 

877 self.endPtsOfContours = endPtsOfContours.tolist() 

878 

879 pos = 2 * self.numberOfContours 

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

881 self.program = ttProgram.Program() 

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

883 pos += 2 + instructionLength 

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

885 flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw( 

886 nCoordinates, data, pos 

887 ) 

888 

889 # fill in repetitions and apply signs 

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

891 xIndex = 0 

892 yIndex = 0 

893 for i in range(nCoordinates): 

894 flag = flags[i] 

895 # x coordinate 

896 if flag & flagXShort: 

897 if flag & flagXsame: 

898 x = xCoordinates[xIndex] 

899 else: 

900 x = -xCoordinates[xIndex] 

901 xIndex = xIndex + 1 

902 elif flag & flagXsame: 

903 x = 0 

904 else: 

905 x = xCoordinates[xIndex] 

906 xIndex = xIndex + 1 

907 # y coordinate 

908 if flag & flagYShort: 

909 if flag & flagYsame: 

910 y = yCoordinates[yIndex] 

911 else: 

912 y = -yCoordinates[yIndex] 

913 yIndex = yIndex + 1 

914 elif flag & flagYsame: 

915 y = 0 

916 else: 

917 y = yCoordinates[yIndex] 

918 yIndex = yIndex + 1 

919 coordinates[i] = (x, y) 

920 assert xIndex == len(xCoordinates) 

921 assert yIndex == len(yCoordinates) 

922 coordinates.relativeToAbsolute() 

923 # discard all flags except "keepFlags" 

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

925 flags[i] &= keepFlags 

926 self.flags = flags 

927 

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

929 # unpack flags and prepare unpacking of coordinates 

930 flags = bytearray(nCoordinates) 

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

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

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

934 xFormat = ">" # big endian 

935 yFormat = ">" # big endian 

936 j = 0 

937 while True: 

938 flag = data[pos] 

939 pos += 1 

940 repeat = 1 

941 if flag & flagRepeat: 

942 repeat = data[pos] + 1 

943 pos += 1 

944 for k in range(repeat): 

945 if flag & flagXShort: 

946 xFormat = xFormat + "B" 

947 elif not (flag & flagXsame): 

948 xFormat = xFormat + "h" 

949 if flag & flagYShort: 

950 yFormat = yFormat + "B" 

951 elif not (flag & flagYsame): 

952 yFormat = yFormat + "h" 

953 flags[j] = flag 

954 j = j + 1 

955 if j >= nCoordinates: 

956 break 

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

958 # unpack raw coordinates, krrrrrr-tching! 

959 xDataLen = struct.calcsize(xFormat) 

960 yDataLen = struct.calcsize(yFormat) 

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

962 log.warning( 

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

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

965 ) 

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

967 yCoordinates = struct.unpack( 

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

969 ) 

970 return flags, xCoordinates, yCoordinates 

971 

972 def compileComponents(self, glyfTable): 

973 data = b"" 

974 lastcomponent = len(self.components) - 1 

975 more = 1 

976 haveInstructions = 0 

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

978 if i == lastcomponent: 

979 haveInstructions = hasattr(self, "program") 

980 more = 0 

981 compo = self.components[i] 

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

983 if haveInstructions: 

984 instructions = self.program.getBytecode() 

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

986 return data 

987 

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

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

990 data = [] 

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

992 if sys.byteorder != "big": 

993 endPtsOfContours.byteswap() 

994 data.append(endPtsOfContours.tobytes()) 

995 instructions = self.program.getBytecode() 

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

997 data.append(instructions) 

998 

999 deltas = self.coordinates.copy() 

1000 deltas.toInt() 

1001 deltas.absoluteToRelative() 

1002 

1003 if optimizeSize: 

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

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

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

1007 else: 

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

1009 

1010 data.extend(deltas) 

1011 return b"".join(data) 

1012 

1013 def compileDeltasGreedy(self, flags, deltas): 

1014 # Implements greedy algorithm for packing coordinate deltas: 

1015 # uses shortest representation one coordinate at a time. 

1016 compressedFlags = bytearray() 

1017 compressedXs = bytearray() 

1018 compressedYs = bytearray() 

1019 lastflag = None 

1020 repeat = 0 

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

1022 # Oh, the horrors of TrueType 

1023 # do x 

1024 if x == 0: 

1025 flag = flag | flagXsame 

1026 elif -255 <= x <= 255: 

1027 flag = flag | flagXShort 

1028 if x > 0: 

1029 flag = flag | flagXsame 

1030 else: 

1031 x = -x 

1032 compressedXs.append(x) 

1033 else: 

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

1035 # do y 

1036 if y == 0: 

1037 flag = flag | flagYsame 

1038 elif -255 <= y <= 255: 

1039 flag = flag | flagYShort 

1040 if y > 0: 

1041 flag = flag | flagYsame 

1042 else: 

1043 y = -y 

1044 compressedYs.append(y) 

1045 else: 

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

1047 # handle repeating flags 

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

1049 repeat = repeat + 1 

1050 if repeat == 1: 

1051 compressedFlags.append(flag) 

1052 else: 

1053 compressedFlags[-2] = flag | flagRepeat 

1054 compressedFlags[-1] = repeat 

1055 else: 

1056 repeat = 0 

1057 compressedFlags.append(flag) 

1058 lastflag = flag 

1059 return (compressedFlags, compressedXs, compressedYs) 

1060 

1061 def compileDeltasOptimal(self, flags, deltas): 

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

1063 # deltas. The savings are negligible :(. 

1064 candidates = [] 

1065 bestTuple = None 

1066 bestCost = 0 

1067 repeat = 0 

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

1069 # Oh, the horrors of TrueType 

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

1071 bestCost += 1 + coordBytes 

1072 newCandidates = [ 

1073 (bestCost, bestTuple, flag, coordBytes), 

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

1075 ] 

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

1077 if ( 

1078 lastCost + coordBytes <= bestCost + 1 

1079 and (lastFlag & flagRepeat) 

1080 and (lastFlag < 0xFF00) 

1081 and flagSupports(lastFlag, flag) 

1082 ): 

1083 if (lastFlag & 0xFF) == ( 

1084 flag | flagRepeat 

1085 ) and lastCost == bestCost + 1: 

1086 continue 

1087 newCandidates.append( 

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

1089 ) 

1090 candidates = newCandidates 

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

1092 bestCost = bestTuple[0] 

1093 

1094 flags = [] 

1095 while bestTuple: 

1096 cost, bestTuple, flag, coordBytes = bestTuple 

1097 flags.append(flag) 

1098 flags.reverse() 

1099 

1100 compressedFlags = bytearray() 

1101 compressedXs = bytearray() 

1102 compressedYs = bytearray() 

1103 coords = iter(deltas) 

1104 ff = [] 

1105 for flag in flags: 

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

1107 compressedFlags.append(flag) 

1108 if flag & flagRepeat: 

1109 assert repeatCount > 0 

1110 compressedFlags.append(repeatCount) 

1111 else: 

1112 assert repeatCount == 0 

1113 for i in range(1 + repeatCount): 

1114 x, y = next(coords) 

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

1116 ff.append(flag) 

1117 try: 

1118 next(coords) 

1119 raise Exception("internal error") 

1120 except StopIteration: 

1121 pass 

1122 

1123 return (compressedFlags, compressedXs, compressedYs) 

1124 

1125 def compileDeltasForSpeed(self, flags, deltas): 

1126 # uses widest representation needed, for all deltas. 

1127 compressedFlags = bytearray() 

1128 compressedXs = bytearray() 

1129 compressedYs = bytearray() 

1130 

1131 # Compute the necessary width for each axis 

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

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

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

1135 xZero = minX == 0 and maxX == 0 

1136 yZero = minY == 0 and maxY == 0 

1137 xShort = -255 <= minX <= maxX <= 255 

1138 yShort = -255 <= minY <= maxY <= 255 

1139 

1140 lastflag = None 

1141 repeat = 0 

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

1143 # Oh, the horrors of TrueType 

1144 # do x 

1145 if xZero: 

1146 flag = flag | flagXsame 

1147 elif xShort: 

1148 flag = flag | flagXShort 

1149 if x > 0: 

1150 flag = flag | flagXsame 

1151 else: 

1152 x = -x 

1153 compressedXs.append(x) 

1154 else: 

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

1156 # do y 

1157 if yZero: 

1158 flag = flag | flagYsame 

1159 elif yShort: 

1160 flag = flag | flagYShort 

1161 if y > 0: 

1162 flag = flag | flagYsame 

1163 else: 

1164 y = -y 

1165 compressedYs.append(y) 

1166 else: 

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

1168 # handle repeating flags 

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

1170 repeat = repeat + 1 

1171 if repeat == 1: 

1172 compressedFlags.append(flag) 

1173 else: 

1174 compressedFlags[-2] = flag | flagRepeat 

1175 compressedFlags[-1] = repeat 

1176 else: 

1177 repeat = 0 

1178 compressedFlags.append(flag) 

1179 lastflag = flag 

1180 return (compressedFlags, compressedXs, compressedYs) 

1181 

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

1183 """Recalculates the bounds of the glyph. 

1184 

1185 Each glyph object stores its bounding box in the 

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

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

1188 must be provided to resolve component bounds. 

1189 """ 

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

1191 glyfTable, boundsDone=boundsDone 

1192 ): 

1193 return 

1194 try: 

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

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

1197 except NotImplementedError: 

1198 pass 

1199 

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

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

1202 certain constrained properties. Namely, none of the components 

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

1204 uses the anchor points. 

1205 

1206 Each glyph object stores its bounding box in the 

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

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

1209 must be provided to resolve component bounds. 

1210 

1211 Return True if bounds were calculated, False otherwise. 

1212 """ 

1213 for compo in self.components: 

1214 if not compo._hasOnlyIntegerTranslate(): 

1215 return False 

1216 

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

1218 bounds = None 

1219 for compo in self.components: 

1220 glyphName = compo.glyphName 

1221 g = glyfTable[glyphName] 

1222 

1223 if boundsDone is None or glyphName not in boundsDone: 

1224 g.recalcBounds(glyfTable, boundsDone=boundsDone) 

1225 if boundsDone is not None: 

1226 boundsDone.add(glyphName) 

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

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

1229 continue 

1230 

1231 x, y = compo.x, compo.y 

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

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

1234 

1235 if bounds is None: 

1236 bounds = (0, 0, 0, 0) 

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

1238 return True 

1239 

1240 def isComposite(self): 

1241 """Test whether a glyph has components""" 

1242 if hasattr(self, "data"): 

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

1244 else: 

1245 return self.numberOfContours == -1 

1246 

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

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

1249 

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

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

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

1253 

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

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

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

1257 in the glyph. 

1258 

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

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

1261 """ 

1262 

1263 if self.numberOfContours > 0: 

1264 return self.coordinates, self.endPtsOfContours, self.flags 

1265 elif self.isComposite(): 

1266 # it's a composite 

1267 allCoords = GlyphCoordinates() 

1268 allFlags = bytearray() 

1269 allEndPts = [] 

1270 for compo in self.components: 

1271 g = glyfTable[compo.glyphName] 

1272 try: 

1273 coordinates, endPts, flags = g.getCoordinates( 

1274 glyfTable, round=round 

1275 ) 

1276 except RecursionError: 

1277 raise ttLib.TTLibError( 

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

1279 % compo.glyphName 

1280 ) 

1281 coordinates = GlyphCoordinates(coordinates) 

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

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

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

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

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

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

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

1289 coordinates.toInt(round=round) 

1290 if hasattr(compo, "firstPt"): 

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

1292 # computing the offset between the points 

1293 if hasattr(compo, "transform"): 

1294 coordinates.transform(compo.transform) 

1295 x1, y1 = allCoords[compo.firstPt] 

1296 x2, y2 = coordinates[compo.secondPt] 

1297 move = x1 - x2, y1 - y2 

1298 coordinates.translate(move) 

1299 else: 

1300 # component uses XY offsets 

1301 move = compo.x, compo.y 

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

1303 coordinates.translate(move) 

1304 else: 

1305 apple_way = compo.flags & SCALED_COMPONENT_OFFSET 

1306 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET 

1307 assert not (apple_way and ms_way) 

1308 if not (apple_way or ms_way): 

1309 scale_component_offset = ( 

1310 SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file 

1311 ) 

1312 else: 

1313 scale_component_offset = apple_way 

1314 if scale_component_offset: 

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

1316 coordinates.translate(move) 

1317 coordinates.transform(compo.transform) 

1318 else: 

1319 # the MS way: first scale, then move 

1320 coordinates.transform(compo.transform) 

1321 coordinates.translate(move) 

1322 offset = len(allCoords) 

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

1324 allCoords.extend(coordinates) 

1325 allFlags.extend(flags) 

1326 return allCoords, allEndPts, allFlags 

1327 else: 

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

1329 

1330 def getComponentNames(self, glyfTable): 

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

1332 

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

1334 empty list) or composite glyphs. 

1335 """ 

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

1337 if self.isComposite(): 

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

1339 else: 

1340 return [] 

1341 

1342 # Extract components without expanding glyph 

1343 

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

1345 return [] # Not composite 

1346 

1347 data = self.data 

1348 i = 10 

1349 components = [] 

1350 more = 1 

1351 while more: 

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

1353 i += 4 

1354 flags = int(flags) 

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

1356 

1357 if flags & ARG_1_AND_2_ARE_WORDS: 

1358 i += 4 

1359 else: 

1360 i += 2 

1361 if flags & WE_HAVE_A_SCALE: 

1362 i += 2 

1363 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1364 i += 4 

1365 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1366 i += 8 

1367 more = flags & MORE_COMPONENTS 

1368 

1369 return components 

1370 

1371 def trim(self, remove_hinting=False): 

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

1373 This works on both expanded and compacted glyphs, without 

1374 expanding it.""" 

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

1376 if remove_hinting: 

1377 if self.isComposite(): 

1378 if hasattr(self, "program"): 

1379 del self.program 

1380 else: 

1381 self.program = ttProgram.Program() 

1382 self.program.fromBytecode([]) 

1383 # No padding to trim. 

1384 return 

1385 if not self.data: 

1386 return 

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

1388 data = bytearray(self.data) 

1389 i = 10 

1390 if numContours >= 0: 

1391 i += 2 * numContours # endPtsOfContours 

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

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

1394 if remove_hinting: 

1395 # Zero instruction length 

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

1397 i += 2 

1398 if instructionLen: 

1399 # Splice it out 

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

1401 instructionLen = 0 

1402 else: 

1403 i += 2 + instructionLen 

1404 

1405 coordBytes = 0 

1406 j = 0 

1407 while True: 

1408 flag = data[i] 

1409 i = i + 1 

1410 repeat = 1 

1411 if flag & flagRepeat: 

1412 repeat = data[i] + 1 

1413 i = i + 1 

1414 xBytes = yBytes = 0 

1415 if flag & flagXShort: 

1416 xBytes = 1 

1417 elif not (flag & flagXsame): 

1418 xBytes = 2 

1419 if flag & flagYShort: 

1420 yBytes = 1 

1421 elif not (flag & flagYsame): 

1422 yBytes = 2 

1423 coordBytes += (xBytes + yBytes) * repeat 

1424 j += repeat 

1425 if j >= nCoordinates: 

1426 break 

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

1428 i += coordBytes 

1429 # Remove padding 

1430 data = data[:i] 

1431 elif self.isComposite(): 

1432 more = 1 

1433 we_have_instructions = False 

1434 while more: 

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

1436 if remove_hinting: 

1437 flags &= ~WE_HAVE_INSTRUCTIONS 

1438 if flags & WE_HAVE_INSTRUCTIONS: 

1439 we_have_instructions = True 

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

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

1442 i += 4 

1443 flags = int(flags) 

1444 

1445 if flags & ARG_1_AND_2_ARE_WORDS: 

1446 i += 4 

1447 else: 

1448 i += 2 

1449 if flags & WE_HAVE_A_SCALE: 

1450 i += 2 

1451 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1452 i += 4 

1453 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1454 i += 8 

1455 more = flags & MORE_COMPONENTS 

1456 if we_have_instructions: 

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

1458 i += 2 + instructionLen 

1459 # Remove padding 

1460 data = data[:i] 

1461 

1462 self.data = data 

1463 

1464 def removeHinting(self): 

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

1466 self.trim(remove_hinting=True) 

1467 

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

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

1470 

1471 Arguments: 

1472 pen: An object conforming to the pen protocol. 

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

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

1475 translated by this offset. 

1476 """ 

1477 

1478 if self.isComposite(): 

1479 for component in self.components: 

1480 glyphName, transform = component.getComponentInfo() 

1481 pen.addComponent(glyphName, transform) 

1482 return 

1483 

1484 self.expand(glyfTable) 

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

1486 if offset: 

1487 coordinates = coordinates.copy() 

1488 coordinates.translate((offset, 0)) 

1489 start = 0 

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

1491 for end in endPts: 

1492 end = end + 1 

1493 contour = coordinates[start:end] 

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

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

1496 start = end 

1497 if 1 not in cFlags: 

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

1499 cubic = all(cuFlags) 

1500 if cubic: 

1501 count = len(contour) 

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

1503 l = contour[-1] 

1504 f = contour[0] 

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

1506 pen.moveTo(p0) 

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

1508 p1 = contour[i] 

1509 p2 = contour[i + 1] 

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

1511 p3 = ( 

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

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

1514 ) 

1515 pen.curveTo(p1, p2, p3) 

1516 else: 

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

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

1519 # as the on-curve point. 

1520 contour.append(None) 

1521 pen.qCurveTo(*contour) 

1522 else: 

1523 # Shuffle the points so that the contour is guaranteed 

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

1525 # the moveTo. 

1526 firstOnCurve = cFlags.index(1) + 1 

1527 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 

1528 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 

1529 cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve] 

1530 pen.moveTo(contour[-1]) 

1531 while contour: 

1532 nextOnCurve = cFlags.index(1) + 1 

1533 if nextOnCurve == 1: 

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

1535 # pen.closePath() 

1536 if len(contour) > 1: 

1537 pen.lineTo(contour[0]) 

1538 else: 

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

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

1541 cubic = any(cubicFlags) 

1542 if cubic: 

1543 assert all( 

1544 cubicFlags 

1545 ), "Mixed cubic and quadratic segment undefined" 

1546 

1547 count = nextOnCurve 

1548 assert ( 

1549 count >= 3 

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

1551 assert ( 

1552 count - 1 

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

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

1555 p1 = contour[i] 

1556 p2 = contour[i + 1] 

1557 p4 = contour[i + 2] 

1558 p3 = ( 

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

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

1561 ) 

1562 lastOnCurve = p3 

1563 pen.curveTo(p1, p2, p3) 

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

1565 else: 

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

1567 contour = contour[nextOnCurve:] 

1568 cFlags = cFlags[nextOnCurve:] 

1569 cuFlags = cuFlags[nextOnCurve:] 

1570 pen.closePath() 

1571 

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

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

1574 this will not change the point indices. 

1575 """ 

1576 

1577 if self.isComposite(): 

1578 for component in self.components: 

1579 glyphName, transform = component.getComponentInfo() 

1580 pen.addComponent(glyphName, transform) 

1581 return 

1582 

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

1584 if offset: 

1585 coordinates = coordinates.copy() 

1586 coordinates.translate((offset, 0)) 

1587 start = 0 

1588 for end in endPts: 

1589 end = end + 1 

1590 contour = coordinates[start:end] 

1591 cFlags = flags[start:end] 

1592 start = end 

1593 pen.beginPath() 

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

1595 

1596 if cFlags[-1] & flagOnCurve: 

1597 segmentType = "line" 

1598 elif cFlags[-1] & flagCubic: 

1599 segmentType = "curve" 

1600 else: 

1601 segmentType = "qcurve" 

1602 for i, pt in enumerate(contour): 

1603 if cFlags[i] & flagOnCurve: 

1604 pen.addPoint(pt, segmentType=segmentType) 

1605 segmentType = "line" 

1606 else: 

1607 pen.addPoint(pt) 

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

1609 pen.endPath() 

1610 

1611 def __eq__(self, other): 

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

1613 return NotImplemented 

1614 return self.__dict__ == other.__dict__ 

1615 

1616 def __ne__(self, other): 

1617 result = self.__eq__(other) 

1618 return result if result is NotImplemented else not result 

1619 

1620 

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

1622# to use otRound below 

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

1624 

1625 

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

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

1628 p0 = Vector(p0) 

1629 p1 = Vector(p1) 

1630 p2 = Vector(p2) 

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

1632 

1633 

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

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

1636 

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

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

1639 

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

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

1642 for all of them will actually be implied. 

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

1644 contours are considered. 

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

1646 

1647 Args: 

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

1649 

1650 Returns: 

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

1652 

1653 Raises: 

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

1655 different point flags or number of contours. 

1656 

1657 Reference: 

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

1659 """ 

1660 staticAttributes = SimpleNamespace( 

1661 numberOfContours=None, flags=None, endPtsOfContours=None 

1662 ) 

1663 drop = None 

1664 simple_glyphs = [] 

1665 for i, glyph in enumerate(interpolatable_glyphs): 

1666 if glyph.numberOfContours < 1: 

1667 # ignore composite or empty glyphs 

1668 continue 

1669 

1670 for attr in staticAttributes.__dict__: 

1671 expected = getattr(staticAttributes, attr) 

1672 found = getattr(glyph, attr) 

1673 if expected is None: 

1674 setattr(staticAttributes, attr, found) 

1675 elif expected != found: 

1676 raise ValueError( 

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

1678 f"expected {expected}, found {found}" 

1679 ) 

1680 

1681 may_drop = set() 

1682 start = 0 

1683 coords = glyph.coordinates 

1684 flags = staticAttributes.flags 

1685 endPtsOfContours = staticAttributes.endPtsOfContours 

1686 for last in endPtsOfContours: 

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

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

1689 continue 

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

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

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

1693 continue 

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

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

1696 continue 

1697 

1698 may_drop.add(i) 

1699 start = last + 1 

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

1701 if drop is None: 

1702 drop = may_drop 

1703 else: 

1704 drop.intersection_update(may_drop) 

1705 

1706 simple_glyphs.append(glyph) 

1707 

1708 if drop: 

1709 # Do the actual dropping 

1710 flags = staticAttributes.flags 

1711 assert flags is not None 

1712 newFlags = array.array( 

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

1714 ) 

1715 

1716 endPts = staticAttributes.endPtsOfContours 

1717 assert endPts is not None 

1718 newEndPts = [] 

1719 i = 0 

1720 delta = 0 

1721 for d in sorted(drop): 

1722 while d > endPts[i]: 

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

1724 i += 1 

1725 delta += 1 

1726 while i < len(endPts): 

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

1728 i += 1 

1729 

1730 for glyph in simple_glyphs: 

1731 coords = glyph.coordinates 

1732 glyph.coordinates = GlyphCoordinates( 

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

1734 ) 

1735 glyph.flags = newFlags 

1736 glyph.endPtsOfContours = newEndPts 

1737 

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

1739 

1740 

1741class GlyphComponent(object): 

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

1743 

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

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

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

1747 attribute is not present. 

1748 """ 

1749 

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

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

1752 # mind - see below. 

1753 

1754 def __init__(self): 

1755 pass 

1756 

1757 def getComponentInfo(self): 

1758 """Return information about the component 

1759 

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

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

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

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

1764 matrix is not present. 

1765 """ 

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

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

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

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

1770 # this TT feature. 

1771 if hasattr(self, "transform"): 

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

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

1774 else: 

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

1776 return self.glyphName, trans 

1777 

1778 def decompile(self, data, glyfTable): 

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

1780 self.flags = int(flags) 

1781 glyphID = int(glyphID) 

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

1783 data = data[4:] 

1784 

1785 if self.flags & ARG_1_AND_2_ARE_WORDS: 

1786 if self.flags & ARGS_ARE_XY_VALUES: 

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

1788 else: 

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

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

1791 data = data[4:] 

1792 else: 

1793 if self.flags & ARGS_ARE_XY_VALUES: 

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

1795 else: 

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

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

1798 data = data[2:] 

1799 

1800 if self.flags & WE_HAVE_A_SCALE: 

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

1802 self.transform = [ 

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

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

1805 ] # fixed 2.14 

1806 data = data[2:] 

1807 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: 

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

1809 self.transform = [ 

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

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

1812 ] # fixed 2.14 

1813 data = data[4:] 

1814 elif self.flags & WE_HAVE_A_TWO_BY_TWO: 

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

1816 self.transform = [ 

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

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

1819 ] # fixed 2.14 

1820 data = data[8:] 

1821 more = self.flags & MORE_COMPONENTS 

1822 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS 

1823 self.flags = self.flags & ( 

1824 ROUND_XY_TO_GRID 

1825 | USE_MY_METRICS 

1826 | SCALED_COMPONENT_OFFSET 

1827 | UNSCALED_COMPONENT_OFFSET 

1828 | NON_OVERLAPPING 

1829 | OVERLAP_COMPOUND 

1830 ) 

1831 return more, haveInstructions, data 

1832 

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

1834 data = b"" 

1835 

1836 # reset all flags we will calculate ourselves 

1837 flags = self.flags & ( 

1838 ROUND_XY_TO_GRID 

1839 | USE_MY_METRICS 

1840 | SCALED_COMPONENT_OFFSET 

1841 | UNSCALED_COMPONENT_OFFSET 

1842 | NON_OVERLAPPING 

1843 | OVERLAP_COMPOUND 

1844 ) 

1845 if more: 

1846 flags = flags | MORE_COMPONENTS 

1847 if haveInstructions: 

1848 flags = flags | WE_HAVE_INSTRUCTIONS 

1849 

1850 if hasattr(self, "firstPt"): 

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

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

1853 else: 

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

1855 flags = flags | ARG_1_AND_2_ARE_WORDS 

1856 else: 

1857 x = otRound(self.x) 

1858 y = otRound(self.y) 

1859 flags = flags | ARGS_ARE_XY_VALUES 

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

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

1862 else: 

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

1864 flags = flags | ARG_1_AND_2_ARE_WORDS 

1865 

1866 if hasattr(self, "transform"): 

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

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

1869 flags = flags | WE_HAVE_A_TWO_BY_TWO 

1870 data = data + struct.pack( 

1871 ">hhhh", 

1872 transform[0][0], 

1873 transform[0][1], 

1874 transform[1][0], 

1875 transform[1][1], 

1876 ) 

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

1878 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE 

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

1880 else: 

1881 flags = flags | WE_HAVE_A_SCALE 

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

1883 

1884 glyphID = glyfTable.getGlyphID(self.glyphName) 

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

1886 

1887 def toXML(self, writer, ttFont): 

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

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

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

1891 else: 

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

1893 

1894 if hasattr(self, "transform"): 

1895 transform = self.transform 

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

1897 attrs = attrs + [ 

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

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

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

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

1902 ] 

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

1904 attrs = attrs + [ 

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

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

1907 ] 

1908 else: 

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

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

1911 writer.simpletag("component", attrs) 

1912 writer.newline() 

1913 

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

1915 self.glyphName = attrs["glyphName"] 

1916 if "firstPt" in attrs: 

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

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

1919 else: 

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

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

1922 if "scale01" in attrs: 

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

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

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

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

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

1928 elif "scalex" in attrs: 

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

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

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

1932 elif "scale" in attrs: 

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

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

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

1936 

1937 def __eq__(self, other): 

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

1939 return NotImplemented 

1940 return self.__dict__ == other.__dict__ 

1941 

1942 def __ne__(self, other): 

1943 result = self.__eq__(other) 

1944 return result if result is NotImplemented else not result 

1945 

1946 def _hasOnlyIntegerTranslate(self): 

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

1948 

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

1950 """ 

1951 return ( 

1952 not hasattr(self, "firstPt") 

1953 and not hasattr(self, "transform") 

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

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

1956 ) 

1957 

1958 

1959class GlyphCoordinates(object): 

1960 """A list of glyph coordinates. 

1961 

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

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

1964 """ 

1965 

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

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

1968 self.extend(iterable) 

1969 

1970 @property 

1971 def array(self): 

1972 """Returns the underlying array of coordinates""" 

1973 return self._a 

1974 

1975 @staticmethod 

1976 def zeros(count): 

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

1978 g = GlyphCoordinates() 

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

1980 return g 

1981 

1982 def copy(self): 

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

1984 c = GlyphCoordinates() 

1985 c._a.extend(self._a) 

1986 return c 

1987 

1988 def __len__(self): 

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

1990 return len(self._a) // 2 

1991 

1992 def __getitem__(self, k): 

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

1994 a = self._a 

1995 if isinstance(k, slice): 

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

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

1998 ret = [] 

1999 for k in indices: 

2000 x = a[2 * k] 

2001 y = a[2 * k + 1] 

2002 ret.append( 

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

2004 ) 

2005 return ret 

2006 x = a[2 * k] 

2007 y = a[2 * k + 1] 

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

2009 

2010 def __setitem__(self, k, v): 

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

2012 if isinstance(k, slice): 

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

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

2015 for j, i in enumerate(indices): 

2016 self[i] = v[j] 

2017 return 

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

2019 

2020 def __delitem__(self, i): 

2021 """Removes a point from the list""" 

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

2023 del self._a[i] 

2024 del self._a[i] 

2025 

2026 def __repr__(self): 

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

2028 

2029 def append(self, p): 

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

2031 

2032 def extend(self, iterable): 

2033 for p in iterable: 

2034 self._a.extend(p) 

2035 

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

2037 if round is noRound: 

2038 return 

2039 a = self._a 

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

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

2042 

2043 def calcBounds(self): 

2044 a = self._a 

2045 if not a: 

2046 return 0, 0, 0, 0 

2047 xs = a[0::2] 

2048 ys = a[1::2] 

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

2050 

2051 def calcIntBounds(self, round=otRound): 

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

2053 

2054 def relativeToAbsolute(self): 

2055 a = self._a 

2056 x, y = 0, 0 

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

2058 a[i] = x = a[i] + x 

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

2060 

2061 def absoluteToRelative(self): 

2062 a = self._a 

2063 x, y = 0, 0 

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

2065 nx = a[i] 

2066 ny = a[i + 1] 

2067 a[i] = nx - x 

2068 a[i + 1] = ny - y 

2069 x = nx 

2070 y = ny 

2071 

2072 def translate(self, p): 

2073 """ 

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

2075 """ 

2076 x, y = p 

2077 if x == 0 and y == 0: 

2078 return 

2079 a = self._a 

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

2081 a[i] += x 

2082 a[i + 1] += y 

2083 

2084 def scale(self, p): 

2085 """ 

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

2087 """ 

2088 x, y = p 

2089 if x == 1 and y == 1: 

2090 return 

2091 a = self._a 

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

2093 a[i] *= x 

2094 a[i + 1] *= y 

2095 

2096 def transform(self, t): 

2097 """ 

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

2099 """ 

2100 a = self._a 

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

2102 x = a[i] 

2103 y = a[i + 1] 

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

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

2106 a[i] = px 

2107 a[i + 1] = py 

2108 

2109 def __eq__(self, other): 

2110 """ 

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

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

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

2114 >>> g == g2 

2115 True 

2116 >>> g == g3 

2117 False 

2118 >>> g2 == g3 

2119 False 

2120 """ 

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

2122 return NotImplemented 

2123 return self._a == other._a 

2124 

2125 def __ne__(self, other): 

2126 """ 

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

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

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

2130 >>> g != g2 

2131 False 

2132 >>> g != g3 

2133 True 

2134 >>> g2 != g3 

2135 True 

2136 """ 

2137 result = self.__eq__(other) 

2138 return result if result is NotImplemented else not result 

2139 

2140 # Math operations 

2141 

2142 def __pos__(self): 

2143 """ 

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

2145 >>> g 

2146 GlyphCoordinates([(1, 2)]) 

2147 >>> g2 = +g 

2148 >>> g2 

2149 GlyphCoordinates([(1, 2)]) 

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

2151 >>> g2 

2152 GlyphCoordinates([(2, 2)]) 

2153 >>> g 

2154 GlyphCoordinates([(1, 2)]) 

2155 """ 

2156 return self.copy() 

2157 

2158 def __neg__(self): 

2159 """ 

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

2161 >>> g 

2162 GlyphCoordinates([(1, 2)]) 

2163 >>> g2 = -g 

2164 >>> g2 

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

2166 >>> g 

2167 GlyphCoordinates([(1, 2)]) 

2168 """ 

2169 r = self.copy() 

2170 a = r._a 

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

2172 a[i] = -a[i] 

2173 return r 

2174 

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

2176 r = self.copy() 

2177 r.toInt(round=round) 

2178 return r 

2179 

2180 def __add__(self, other): 

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

2182 

2183 def __sub__(self, other): 

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

2185 

2186 def __mul__(self, other): 

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

2188 

2189 def __truediv__(self, other): 

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

2191 

2192 __radd__ = __add__ 

2193 __rmul__ = __mul__ 

2194 

2195 def __rsub__(self, other): 

2196 return other + (-self) 

2197 

2198 def __iadd__(self, other): 

2199 """ 

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

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

2202 >>> g 

2203 GlyphCoordinates([(1.5, 2)]) 

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

2205 >>> g += g2 

2206 >>> g 

2207 GlyphCoordinates([(4.5, 6)]) 

2208 """ 

2209 if isinstance(other, tuple): 

2210 assert len(other) == 2 

2211 self.translate(other) 

2212 return self 

2213 if isinstance(other, GlyphCoordinates): 

2214 other = other._a 

2215 a = self._a 

2216 assert len(a) == len(other) 

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

2218 a[i] += other[i] 

2219 return self 

2220 return NotImplemented 

2221 

2222 def __isub__(self, other): 

2223 """ 

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

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

2226 >>> g 

2227 GlyphCoordinates([(0.5, 2)]) 

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

2229 >>> g -= g2 

2230 >>> g 

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

2232 """ 

2233 if isinstance(other, tuple): 

2234 assert len(other) == 2 

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

2236 return self 

2237 if isinstance(other, GlyphCoordinates): 

2238 other = other._a 

2239 a = self._a 

2240 assert len(a) == len(other) 

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

2242 a[i] -= other[i] 

2243 return self 

2244 return NotImplemented 

2245 

2246 def __imul__(self, other): 

2247 """ 

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

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

2250 >>> g *= 2 

2251 >>> g 

2252 GlyphCoordinates([(4, 2)]) 

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

2254 >>> g *= 2 

2255 >>> g 

2256 GlyphCoordinates([(2, 4)]) 

2257 """ 

2258 if isinstance(other, tuple): 

2259 assert len(other) == 2 

2260 self.scale(other) 

2261 return self 

2262 if isinstance(other, Number): 

2263 if other == 1: 

2264 return self 

2265 a = self._a 

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

2267 a[i] *= other 

2268 return self 

2269 return NotImplemented 

2270 

2271 def __itruediv__(self, other): 

2272 """ 

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

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

2275 >>> g /= 2 

2276 >>> g 

2277 GlyphCoordinates([(1, 1)]) 

2278 """ 

2279 if isinstance(other, Number): 

2280 other = (other, other) 

2281 if isinstance(other, tuple): 

2282 if other == (1, 1): 

2283 return self 

2284 assert len(other) == 2 

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

2286 return self 

2287 return NotImplemented 

2288 

2289 def __bool__(self): 

2290 """ 

2291 >>> g = GlyphCoordinates([]) 

2292 >>> bool(g) 

2293 False 

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

2295 >>> bool(g) 

2296 True 

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

2298 >>> bool(g) 

2299 True 

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

2301 >>> bool(g) 

2302 True 

2303 """ 

2304 return bool(self._a) 

2305 

2306 __nonzero__ = __bool__ 

2307 

2308 

2309if __name__ == "__main__": 

2310 import doctest, sys 

2311 

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