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

1398 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, compo in enumerate(self.components): 

978 if i == lastcomponent: 

979 haveInstructions = hasattr(self, "program") 

980 more = 0 

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

982 if haveInstructions: 

983 instructions = self.program.getBytecode() 

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

985 return data 

986 

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

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

989 data = [] 

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

991 if sys.byteorder != "big": 

992 endPtsOfContours.byteswap() 

993 data.append(endPtsOfContours.tobytes()) 

994 instructions = self.program.getBytecode() 

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

996 data.append(instructions) 

997 

998 deltas = self.coordinates.copy() 

999 deltas.toInt() 

1000 deltas.absoluteToRelative() 

1001 

1002 if optimizeSize: 

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

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

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

1006 else: 

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

1008 

1009 data.extend(deltas) 

1010 return b"".join(data) 

1011 

1012 def compileDeltasGreedy(self, flags, deltas): 

1013 # Implements greedy algorithm for packing coordinate deltas: 

1014 # uses shortest representation one coordinate at a time. 

1015 compressedFlags = bytearray() 

1016 compressedXs = bytearray() 

1017 compressedYs = bytearray() 

1018 lastflag = None 

1019 repeat = 0 

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

1021 # Oh, the horrors of TrueType 

1022 # do x 

1023 if x == 0: 

1024 flag = flag | flagXsame 

1025 elif -255 <= x <= 255: 

1026 flag = flag | flagXShort 

1027 if x > 0: 

1028 flag = flag | flagXsame 

1029 else: 

1030 x = -x 

1031 compressedXs.append(x) 

1032 else: 

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

1034 # do y 

1035 if y == 0: 

1036 flag = flag | flagYsame 

1037 elif -255 <= y <= 255: 

1038 flag = flag | flagYShort 

1039 if y > 0: 

1040 flag = flag | flagYsame 

1041 else: 

1042 y = -y 

1043 compressedYs.append(y) 

1044 else: 

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

1046 # handle repeating flags 

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

1048 repeat = repeat + 1 

1049 if repeat == 1: 

1050 compressedFlags.append(flag) 

1051 else: 

1052 compressedFlags[-2] = flag | flagRepeat 

1053 compressedFlags[-1] = repeat 

1054 else: 

1055 repeat = 0 

1056 compressedFlags.append(flag) 

1057 lastflag = flag 

1058 return (compressedFlags, compressedXs, compressedYs) 

1059 

1060 def compileDeltasOptimal(self, flags, deltas): 

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

1062 # deltas. The savings are negligible :(. 

1063 candidates = [] 

1064 bestTuple = None 

1065 bestCost = 0 

1066 repeat = 0 

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

1068 # Oh, the horrors of TrueType 

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

1070 bestCost += 1 + coordBytes 

1071 newCandidates = [ 

1072 (bestCost, bestTuple, flag, coordBytes), 

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

1074 ] 

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

1076 if ( 

1077 lastCost + coordBytes <= bestCost + 1 

1078 and (lastFlag & flagRepeat) 

1079 and (lastFlag < 0xFF00) 

1080 and flagSupports(lastFlag, flag) 

1081 ): 

1082 if (lastFlag & 0xFF) == ( 

1083 flag | flagRepeat 

1084 ) and lastCost == bestCost + 1: 

1085 continue 

1086 newCandidates.append( 

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

1088 ) 

1089 candidates = newCandidates 

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

1091 bestCost = bestTuple[0] 

1092 

1093 flags = [] 

1094 while bestTuple: 

1095 cost, bestTuple, flag, coordBytes = bestTuple 

1096 flags.append(flag) 

1097 flags.reverse() 

1098 

1099 compressedFlags = bytearray() 

1100 compressedXs = bytearray() 

1101 compressedYs = bytearray() 

1102 coords = iter(deltas) 

1103 ff = [] 

1104 for flag in flags: 

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

1106 compressedFlags.append(flag) 

1107 if flag & flagRepeat: 

1108 assert repeatCount > 0 

1109 compressedFlags.append(repeatCount) 

1110 else: 

1111 assert repeatCount == 0 

1112 for i in range(1 + repeatCount): 

1113 x, y = next(coords) 

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

1115 ff.append(flag) 

1116 try: 

1117 next(coords) 

1118 raise Exception("internal error") 

1119 except StopIteration: 

1120 pass 

1121 

1122 return (compressedFlags, compressedXs, compressedYs) 

1123 

1124 def compileDeltasForSpeed(self, flags, deltas): 

1125 # uses widest representation needed, for all deltas. 

1126 compressedFlags = bytearray() 

1127 compressedXs = bytearray() 

1128 compressedYs = bytearray() 

1129 

1130 # Compute the necessary width for each axis 

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

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

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

1134 xZero = minX == 0 and maxX == 0 

1135 yZero = minY == 0 and maxY == 0 

1136 xShort = -255 <= minX <= maxX <= 255 

1137 yShort = -255 <= minY <= maxY <= 255 

1138 

1139 lastflag = None 

1140 repeat = 0 

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

1142 # Oh, the horrors of TrueType 

1143 # do x 

1144 if xZero: 

1145 flag = flag | flagXsame 

1146 elif xShort: 

1147 flag = flag | flagXShort 

1148 if x > 0: 

1149 flag = flag | flagXsame 

1150 else: 

1151 x = -x 

1152 compressedXs.append(x) 

1153 else: 

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

1155 # do y 

1156 if yZero: 

1157 flag = flag | flagYsame 

1158 elif yShort: 

1159 flag = flag | flagYShort 

1160 if y > 0: 

1161 flag = flag | flagYsame 

1162 else: 

1163 y = -y 

1164 compressedYs.append(y) 

1165 else: 

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

1167 # handle repeating flags 

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

1169 repeat = repeat + 1 

1170 if repeat == 1: 

1171 compressedFlags.append(flag) 

1172 else: 

1173 compressedFlags[-2] = flag | flagRepeat 

1174 compressedFlags[-1] = repeat 

1175 else: 

1176 repeat = 0 

1177 compressedFlags.append(flag) 

1178 lastflag = flag 

1179 return (compressedFlags, compressedXs, compressedYs) 

1180 

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

1182 """Recalculates the bounds of the glyph. 

1183 

1184 Each glyph object stores its bounding box in the 

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

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

1187 must be provided to resolve component bounds. 

1188 """ 

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

1190 glyfTable, boundsDone=boundsDone 

1191 ): 

1192 return 

1193 try: 

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

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

1196 except NotImplementedError: 

1197 pass 

1198 

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

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

1201 certain constrained properties. Namely, none of the components 

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

1203 uses the anchor points. 

1204 

1205 Each glyph object stores its bounding box in the 

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

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

1208 must be provided to resolve component bounds. 

1209 

1210 Return True if bounds were calculated, False otherwise. 

1211 """ 

1212 for compo in self.components: 

1213 if not compo._hasOnlyIntegerTranslate(): 

1214 return False 

1215 

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

1217 bounds = None 

1218 for compo in self.components: 

1219 glyphName = compo.glyphName 

1220 g = glyfTable[glyphName] 

1221 

1222 if boundsDone is None or glyphName not in boundsDone: 

1223 g.recalcBounds(glyfTable, boundsDone=boundsDone) 

1224 if boundsDone is not None: 

1225 boundsDone.add(glyphName) 

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

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

1228 continue 

1229 

1230 x, y = compo.x, compo.y 

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

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

1233 

1234 if bounds is None: 

1235 bounds = (0, 0, 0, 0) 

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

1237 return True 

1238 

1239 def isComposite(self): 

1240 """Test whether a glyph has components""" 

1241 if hasattr(self, "data"): 

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

1243 else: 

1244 return self.numberOfContours == -1 

1245 

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

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

1248 

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

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

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

1252 

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

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

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

1256 in the glyph. 

1257 

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

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

1260 """ 

1261 

1262 if self.numberOfContours > 0: 

1263 return self.coordinates, self.endPtsOfContours, self.flags 

1264 elif self.isComposite(): 

1265 # it's a composite 

1266 allCoords = GlyphCoordinates() 

1267 allFlags = bytearray() 

1268 allEndPts = [] 

1269 for compo in self.components: 

1270 g = glyfTable[compo.glyphName] 

1271 try: 

1272 coordinates, endPts, flags = g.getCoordinates( 

1273 glyfTable, round=round 

1274 ) 

1275 except RecursionError: 

1276 raise ttLib.TTLibError( 

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

1278 % compo.glyphName 

1279 ) 

1280 coordinates = GlyphCoordinates(coordinates) 

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

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

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

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

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

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

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

1288 coordinates.toInt(round=round) 

1289 if hasattr(compo, "firstPt"): 

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

1291 # computing the offset between the points 

1292 if hasattr(compo, "transform"): 

1293 coordinates.transform(compo.transform) 

1294 x1, y1 = allCoords[compo.firstPt] 

1295 x2, y2 = coordinates[compo.secondPt] 

1296 move = x1 - x2, y1 - y2 

1297 coordinates.translate(move) 

1298 else: 

1299 # component uses XY offsets 

1300 move = compo.x, compo.y 

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

1302 coordinates.translate(move) 

1303 else: 

1304 apple_way = compo.flags & SCALED_COMPONENT_OFFSET 

1305 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET 

1306 assert not (apple_way and ms_way) 

1307 if not (apple_way or ms_way): 

1308 scale_component_offset = ( 

1309 SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file 

1310 ) 

1311 else: 

1312 scale_component_offset = apple_way 

1313 if scale_component_offset: 

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

1315 coordinates.translate(move) 

1316 coordinates.transform(compo.transform) 

1317 else: 

1318 # the MS way: first scale, then move 

1319 coordinates.transform(compo.transform) 

1320 coordinates.translate(move) 

1321 offset = len(allCoords) 

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

1323 allCoords.extend(coordinates) 

1324 allFlags.extend(flags) 

1325 return allCoords, allEndPts, allFlags 

1326 else: 

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

1328 

1329 def getComponentNames(self, glyfTable): 

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

1331 

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

1333 empty list) or composite glyphs. 

1334 """ 

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

1336 if self.isComposite(): 

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

1338 else: 

1339 return [] 

1340 

1341 # Extract components without expanding glyph 

1342 

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

1344 return [] # Not composite 

1345 

1346 data = self.data 

1347 i = 10 

1348 components = [] 

1349 more = 1 

1350 while more: 

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

1352 i += 4 

1353 flags = int(flags) 

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

1355 

1356 if flags & ARG_1_AND_2_ARE_WORDS: 

1357 i += 4 

1358 else: 

1359 i += 2 

1360 if flags & WE_HAVE_A_SCALE: 

1361 i += 2 

1362 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1363 i += 4 

1364 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1365 i += 8 

1366 more = flags & MORE_COMPONENTS 

1367 

1368 return components 

1369 

1370 def trim(self, remove_hinting=False): 

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

1372 This works on both expanded and compacted glyphs, without 

1373 expanding it.""" 

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

1375 if remove_hinting: 

1376 if self.isComposite(): 

1377 if hasattr(self, "program"): 

1378 del self.program 

1379 else: 

1380 self.program = ttProgram.Program() 

1381 self.program.fromBytecode([]) 

1382 # No padding to trim. 

1383 return 

1384 if not self.data: 

1385 return 

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

1387 data = bytearray(self.data) 

1388 i = 10 

1389 if numContours >= 0: 

1390 i += 2 * numContours # endPtsOfContours 

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

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

1393 if remove_hinting: 

1394 # Zero instruction length 

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

1396 i += 2 

1397 if instructionLen: 

1398 # Splice it out 

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

1400 instructionLen = 0 

1401 else: 

1402 i += 2 + instructionLen 

1403 

1404 coordBytes = 0 

1405 j = 0 

1406 while True: 

1407 flag = data[i] 

1408 i = i + 1 

1409 repeat = 1 

1410 if flag & flagRepeat: 

1411 repeat = data[i] + 1 

1412 i = i + 1 

1413 xBytes = yBytes = 0 

1414 if flag & flagXShort: 

1415 xBytes = 1 

1416 elif not (flag & flagXsame): 

1417 xBytes = 2 

1418 if flag & flagYShort: 

1419 yBytes = 1 

1420 elif not (flag & flagYsame): 

1421 yBytes = 2 

1422 coordBytes += (xBytes + yBytes) * repeat 

1423 j += repeat 

1424 if j >= nCoordinates: 

1425 break 

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

1427 i += coordBytes 

1428 # Remove padding 

1429 data = data[:i] 

1430 elif self.isComposite(): 

1431 more = 1 

1432 we_have_instructions = False 

1433 while more: 

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

1435 if remove_hinting: 

1436 flags &= ~WE_HAVE_INSTRUCTIONS 

1437 if flags & WE_HAVE_INSTRUCTIONS: 

1438 we_have_instructions = True 

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

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

1441 i += 4 

1442 flags = int(flags) 

1443 

1444 if flags & ARG_1_AND_2_ARE_WORDS: 

1445 i += 4 

1446 else: 

1447 i += 2 

1448 if flags & WE_HAVE_A_SCALE: 

1449 i += 2 

1450 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 

1451 i += 4 

1452 elif flags & WE_HAVE_A_TWO_BY_TWO: 

1453 i += 8 

1454 more = flags & MORE_COMPONENTS 

1455 if we_have_instructions: 

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

1457 i += 2 + instructionLen 

1458 # Remove padding 

1459 data = data[:i] 

1460 

1461 self.data = data 

1462 

1463 def removeHinting(self): 

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

1465 self.trim(remove_hinting=True) 

1466 

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

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

1469 

1470 Arguments: 

1471 pen: An object conforming to the pen protocol. 

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

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

1474 translated by this offset. 

1475 """ 

1476 

1477 if self.isComposite(): 

1478 for component in self.components: 

1479 glyphName, transform = component.getComponentInfo() 

1480 pen.addComponent(glyphName, transform) 

1481 return 

1482 

1483 self.expand(glyfTable) 

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

1485 if offset: 

1486 coordinates = coordinates.copy() 

1487 coordinates.translate((offset, 0)) 

1488 start = 0 

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

1490 for end in endPts: 

1491 end = end + 1 

1492 contour = coordinates[start:end] 

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

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

1495 start = end 

1496 if 1 not in cFlags: 

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

1498 cubic = all(cuFlags) 

1499 if cubic: 

1500 count = len(contour) 

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

1502 l = contour[-1] 

1503 f = contour[0] 

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

1505 pen.moveTo(p0) 

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

1507 p1 = contour[i] 

1508 p2 = contour[i + 1] 

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

1510 p3 = ( 

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

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

1513 ) 

1514 pen.curveTo(p1, p2, p3) 

1515 else: 

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

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

1518 # as the on-curve point. 

1519 contour.append(None) 

1520 pen.qCurveTo(*contour) 

1521 else: 

1522 # Shuffle the points so that the contour is guaranteed 

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

1524 # the moveTo. 

1525 firstOnCurve = cFlags.index(1) + 1 

1526 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 

1527 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 

1528 cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve] 

1529 pen.moveTo(contour[-1]) 

1530 while contour: 

1531 nextOnCurve = cFlags.index(1) + 1 

1532 if nextOnCurve == 1: 

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

1534 # pen.closePath() 

1535 if len(contour) > 1: 

1536 pen.lineTo(contour[0]) 

1537 else: 

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

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

1540 cubic = any(cubicFlags) 

1541 if cubic: 

1542 assert all( 

1543 cubicFlags 

1544 ), "Mixed cubic and quadratic segment undefined" 

1545 

1546 count = nextOnCurve 

1547 assert ( 

1548 count >= 3 

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

1550 assert ( 

1551 count - 1 

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

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

1554 p1 = contour[i] 

1555 p2 = contour[i + 1] 

1556 p4 = contour[i + 2] 

1557 p3 = ( 

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

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

1560 ) 

1561 lastOnCurve = p3 

1562 pen.curveTo(p1, p2, p3) 

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

1564 else: 

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

1566 contour = contour[nextOnCurve:] 

1567 cFlags = cFlags[nextOnCurve:] 

1568 cuFlags = cuFlags[nextOnCurve:] 

1569 pen.closePath() 

1570 

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

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

1573 this will not change the point indices. 

1574 """ 

1575 

1576 if self.isComposite(): 

1577 for component in self.components: 

1578 glyphName, transform = component.getComponentInfo() 

1579 pen.addComponent(glyphName, transform) 

1580 return 

1581 

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

1583 if offset: 

1584 coordinates = coordinates.copy() 

1585 coordinates.translate((offset, 0)) 

1586 start = 0 

1587 for end in endPts: 

1588 end = end + 1 

1589 contour = coordinates[start:end] 

1590 cFlags = flags[start:end] 

1591 start = end 

1592 pen.beginPath() 

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

1594 

1595 if cFlags[-1] & flagOnCurve: 

1596 segmentType = "line" 

1597 elif cFlags[-1] & flagCubic: 

1598 segmentType = "curve" 

1599 else: 

1600 segmentType = "qcurve" 

1601 for i, pt in enumerate(contour): 

1602 if cFlags[i] & flagOnCurve: 

1603 pen.addPoint(pt, segmentType=segmentType) 

1604 segmentType = "line" 

1605 else: 

1606 pen.addPoint(pt) 

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

1608 pen.endPath() 

1609 

1610 def __eq__(self, other): 

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

1612 return NotImplemented 

1613 return self.__dict__ == other.__dict__ 

1614 

1615 def __ne__(self, other): 

1616 result = self.__eq__(other) 

1617 return result if result is NotImplemented else not result 

1618 

1619 

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

1621# to use otRound below 

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

1623 

1624 

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

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

1627 p0 = Vector(p0) 

1628 p1 = Vector(p1) 

1629 p2 = Vector(p2) 

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

1631 

1632 

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

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

1635 

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

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

1638 

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

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

1641 for all of them will actually be implied. 

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

1643 contours are considered. 

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

1645 

1646 Args: 

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

1648 

1649 Returns: 

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

1651 

1652 Raises: 

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

1654 different point flags or number of contours. 

1655 

1656 Reference: 

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

1658 """ 

1659 staticAttributes = SimpleNamespace( 

1660 numberOfContours=None, flags=None, endPtsOfContours=None 

1661 ) 

1662 drop = None 

1663 simple_glyphs = [] 

1664 for i, glyph in enumerate(interpolatable_glyphs): 

1665 if glyph.numberOfContours < 1: 

1666 # ignore composite or empty glyphs 

1667 continue 

1668 

1669 for attr in staticAttributes.__dict__: 

1670 expected = getattr(staticAttributes, attr) 

1671 found = getattr(glyph, attr) 

1672 if expected is None: 

1673 setattr(staticAttributes, attr, found) 

1674 elif expected != found: 

1675 raise ValueError( 

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

1677 f"expected {expected}, found {found}" 

1678 ) 

1679 

1680 may_drop = set() 

1681 start = 0 

1682 coords = glyph.coordinates 

1683 flags = staticAttributes.flags 

1684 endPtsOfContours = staticAttributes.endPtsOfContours 

1685 for last in endPtsOfContours: 

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

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

1688 continue 

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

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

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

1692 continue 

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

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

1695 continue 

1696 

1697 may_drop.add(i) 

1698 start = last + 1 

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

1700 if drop is None: 

1701 drop = may_drop 

1702 else: 

1703 drop.intersection_update(may_drop) 

1704 

1705 simple_glyphs.append(glyph) 

1706 

1707 if drop: 

1708 # Do the actual dropping 

1709 flags = staticAttributes.flags 

1710 assert flags is not None 

1711 newFlags = array.array( 

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

1713 ) 

1714 

1715 endPts = staticAttributes.endPtsOfContours 

1716 assert endPts is not None 

1717 newEndPts = [] 

1718 i = 0 

1719 delta = 0 

1720 for d in sorted(drop): 

1721 while d > endPts[i]: 

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

1723 i += 1 

1724 delta += 1 

1725 while i < len(endPts): 

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

1727 i += 1 

1728 

1729 for glyph in simple_glyphs: 

1730 coords = glyph.coordinates 

1731 glyph.coordinates = GlyphCoordinates( 

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

1733 ) 

1734 glyph.flags = newFlags 

1735 glyph.endPtsOfContours = newEndPts 

1736 

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

1738 

1739 

1740class GlyphComponent(object): 

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

1742 

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

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

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

1746 attribute is not present. 

1747 """ 

1748 

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

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

1751 # mind - see below. 

1752 

1753 def __init__(self): 

1754 pass 

1755 

1756 def getComponentInfo(self): 

1757 """Return information about the component 

1758 

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

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

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

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

1763 matrix is not present. 

1764 """ 

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

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

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

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

1769 # this TT feature. 

1770 if hasattr(self, "transform"): 

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

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

1773 else: 

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

1775 return self.glyphName, trans 

1776 

1777 def decompile(self, data, glyfTable): 

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

1779 self.flags = int(flags) 

1780 glyphID = int(glyphID) 

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

1782 data = data[4:] 

1783 

1784 if self.flags & ARG_1_AND_2_ARE_WORDS: 

1785 if self.flags & ARGS_ARE_XY_VALUES: 

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

1787 else: 

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

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

1790 data = data[4:] 

1791 else: 

1792 if self.flags & ARGS_ARE_XY_VALUES: 

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

1794 else: 

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

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

1797 data = data[2:] 

1798 

1799 if self.flags & WE_HAVE_A_SCALE: 

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

1801 self.transform = [ 

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

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

1804 ] # fixed 2.14 

1805 data = data[2:] 

1806 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: 

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

1808 self.transform = [ 

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

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

1811 ] # fixed 2.14 

1812 data = data[4:] 

1813 elif self.flags & WE_HAVE_A_TWO_BY_TWO: 

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

1815 self.transform = [ 

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

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

1818 ] # fixed 2.14 

1819 data = data[8:] 

1820 more = self.flags & MORE_COMPONENTS 

1821 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS 

1822 self.flags = self.flags & ( 

1823 ROUND_XY_TO_GRID 

1824 | USE_MY_METRICS 

1825 | SCALED_COMPONENT_OFFSET 

1826 | UNSCALED_COMPONENT_OFFSET 

1827 | NON_OVERLAPPING 

1828 | OVERLAP_COMPOUND 

1829 ) 

1830 return more, haveInstructions, data 

1831 

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

1833 data = b"" 

1834 

1835 # reset all flags we will calculate ourselves 

1836 flags = self.flags & ( 

1837 ROUND_XY_TO_GRID 

1838 | USE_MY_METRICS 

1839 | SCALED_COMPONENT_OFFSET 

1840 | UNSCALED_COMPONENT_OFFSET 

1841 | NON_OVERLAPPING 

1842 | OVERLAP_COMPOUND 

1843 ) 

1844 if more: 

1845 flags = flags | MORE_COMPONENTS 

1846 if haveInstructions: 

1847 flags = flags | WE_HAVE_INSTRUCTIONS 

1848 

1849 if hasattr(self, "firstPt"): 

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

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

1852 else: 

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

1854 flags = flags | ARG_1_AND_2_ARE_WORDS 

1855 else: 

1856 x = otRound(self.x) 

1857 y = otRound(self.y) 

1858 flags = flags | ARGS_ARE_XY_VALUES 

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

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

1861 else: 

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

1863 flags = flags | ARG_1_AND_2_ARE_WORDS 

1864 

1865 if hasattr(self, "transform"): 

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

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

1868 flags = flags | WE_HAVE_A_TWO_BY_TWO 

1869 data = data + struct.pack( 

1870 ">hhhh", 

1871 transform[0][0], 

1872 transform[0][1], 

1873 transform[1][0], 

1874 transform[1][1], 

1875 ) 

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

1877 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE 

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

1879 else: 

1880 flags = flags | WE_HAVE_A_SCALE 

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

1882 

1883 glyphID = glyfTable.getGlyphID(self.glyphName) 

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

1885 

1886 def toXML(self, writer, ttFont): 

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

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

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

1890 else: 

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

1892 

1893 if hasattr(self, "transform"): 

1894 transform = self.transform 

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

1896 attrs = attrs + [ 

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

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

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

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

1901 ] 

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

1903 attrs = attrs + [ 

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

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

1906 ] 

1907 else: 

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

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

1910 writer.simpletag("component", attrs) 

1911 writer.newline() 

1912 

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

1914 self.glyphName = attrs["glyphName"] 

1915 if "firstPt" in attrs: 

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

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

1918 else: 

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

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

1921 if "scale01" in attrs: 

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

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

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

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

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

1927 elif "scalex" in attrs: 

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

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

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

1931 elif "scale" in attrs: 

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

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

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

1935 

1936 def __eq__(self, other): 

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

1938 return NotImplemented 

1939 return self.__dict__ == other.__dict__ 

1940 

1941 def __ne__(self, other): 

1942 result = self.__eq__(other) 

1943 return result if result is NotImplemented else not result 

1944 

1945 def _hasOnlyIntegerTranslate(self): 

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

1947 

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

1949 """ 

1950 return ( 

1951 not hasattr(self, "firstPt") 

1952 and not hasattr(self, "transform") 

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

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

1955 ) 

1956 

1957 

1958class GlyphCoordinates(object): 

1959 """A list of glyph coordinates. 

1960 

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

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

1963 """ 

1964 

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

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

1967 self.extend(iterable) 

1968 

1969 @property 

1970 def array(self): 

1971 """Returns the underlying array of coordinates""" 

1972 return self._a 

1973 

1974 @staticmethod 

1975 def zeros(count): 

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

1977 g = GlyphCoordinates() 

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

1979 return g 

1980 

1981 def copy(self): 

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

1983 c = GlyphCoordinates() 

1984 c._a.extend(self._a) 

1985 return c 

1986 

1987 def __len__(self): 

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

1989 return len(self._a) // 2 

1990 

1991 def __getitem__(self, k): 

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

1993 a = self._a 

1994 if isinstance(k, slice): 

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

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

1997 ret = [] 

1998 for k in indices: 

1999 x = a[2 * k] 

2000 y = a[2 * k + 1] 

2001 ret.append( 

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

2003 ) 

2004 return ret 

2005 x = a[2 * k] 

2006 y = a[2 * k + 1] 

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

2008 

2009 def __setitem__(self, k, v): 

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

2011 if isinstance(k, slice): 

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

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

2014 for j, i in enumerate(indices): 

2015 self[i] = v[j] 

2016 return 

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

2018 

2019 def __delitem__(self, i): 

2020 """Removes a point from the list""" 

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

2022 del self._a[i] 

2023 del self._a[i] 

2024 

2025 def __repr__(self): 

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

2027 

2028 def append(self, p): 

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

2030 

2031 def extend(self, iterable): 

2032 for p in iterable: 

2033 self._a.extend(p) 

2034 

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

2036 if round is noRound: 

2037 return 

2038 a = self._a 

2039 for i, value in enumerate(a): 

2040 a[i] = round(value) 

2041 

2042 def calcBounds(self): 

2043 a = self._a 

2044 if not a: 

2045 return 0, 0, 0, 0 

2046 xs = a[0::2] 

2047 ys = a[1::2] 

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

2049 

2050 def calcIntBounds(self, round=otRound): 

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

2052 

2053 def relativeToAbsolute(self): 

2054 a = self._a 

2055 x, y = 0, 0 

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

2057 a[i] = x = a[i] + x 

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

2059 

2060 def absoluteToRelative(self): 

2061 a = self._a 

2062 x, y = 0, 0 

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

2064 nx = a[i] 

2065 ny = a[i + 1] 

2066 a[i] = nx - x 

2067 a[i + 1] = ny - y 

2068 x = nx 

2069 y = ny 

2070 

2071 def translate(self, p): 

2072 """ 

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

2074 """ 

2075 x, y = p 

2076 if x == 0 and y == 0: 

2077 return 

2078 a = self._a 

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

2080 a[i] += x 

2081 a[i + 1] += y 

2082 

2083 def scale(self, p): 

2084 """ 

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

2086 """ 

2087 x, y = p 

2088 if x == 1 and y == 1: 

2089 return 

2090 a = self._a 

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

2092 a[i] *= x 

2093 a[i + 1] *= y 

2094 

2095 def transform(self, t): 

2096 """ 

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

2098 """ 

2099 a = self._a 

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

2101 x = a[i] 

2102 y = a[i + 1] 

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

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

2105 a[i] = px 

2106 a[i + 1] = py 

2107 

2108 def __eq__(self, other): 

2109 """ 

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

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

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

2113 >>> g == g2 

2114 True 

2115 >>> g == g3 

2116 False 

2117 >>> g2 == g3 

2118 False 

2119 """ 

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

2121 return NotImplemented 

2122 return self._a == other._a 

2123 

2124 def __ne__(self, other): 

2125 """ 

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

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

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

2129 >>> g != g2 

2130 False 

2131 >>> g != g3 

2132 True 

2133 >>> g2 != g3 

2134 True 

2135 """ 

2136 result = self.__eq__(other) 

2137 return result if result is NotImplemented else not result 

2138 

2139 # Math operations 

2140 

2141 def __pos__(self): 

2142 """ 

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

2144 >>> g 

2145 GlyphCoordinates([(1, 2)]) 

2146 >>> g2 = +g 

2147 >>> g2 

2148 GlyphCoordinates([(1, 2)]) 

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

2150 >>> g2 

2151 GlyphCoordinates([(2, 2)]) 

2152 >>> g 

2153 GlyphCoordinates([(1, 2)]) 

2154 """ 

2155 return self.copy() 

2156 

2157 def __neg__(self): 

2158 """ 

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

2160 >>> g 

2161 GlyphCoordinates([(1, 2)]) 

2162 >>> g2 = -g 

2163 >>> g2 

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

2165 >>> g 

2166 GlyphCoordinates([(1, 2)]) 

2167 """ 

2168 r = self.copy() 

2169 a = r._a 

2170 for i, value in enumerate(a): 

2171 a[i] = -value 

2172 return r 

2173 

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

2175 r = self.copy() 

2176 r.toInt(round=round) 

2177 return r 

2178 

2179 def __add__(self, other): 

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

2181 

2182 def __sub__(self, other): 

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

2184 

2185 def __mul__(self, other): 

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

2187 

2188 def __truediv__(self, other): 

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

2190 

2191 __radd__ = __add__ 

2192 __rmul__ = __mul__ 

2193 

2194 def __rsub__(self, other): 

2195 return other + (-self) 

2196 

2197 def __iadd__(self, other): 

2198 """ 

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

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

2201 >>> g 

2202 GlyphCoordinates([(1.5, 2)]) 

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

2204 >>> g += g2 

2205 >>> g 

2206 GlyphCoordinates([(4.5, 6)]) 

2207 """ 

2208 if isinstance(other, tuple): 

2209 assert len(other) == 2 

2210 self.translate(other) 

2211 return self 

2212 if isinstance(other, GlyphCoordinates): 

2213 other = other._a 

2214 a = self._a 

2215 assert len(a) == len(other) 

2216 for i, value in enumerate(other): 

2217 a[i] += value 

2218 return self 

2219 return NotImplemented 

2220 

2221 def __isub__(self, other): 

2222 """ 

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

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

2225 >>> g 

2226 GlyphCoordinates([(0.5, 2)]) 

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

2228 >>> g -= g2 

2229 >>> g 

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

2231 """ 

2232 if isinstance(other, tuple): 

2233 assert len(other) == 2 

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

2235 return self 

2236 if isinstance(other, GlyphCoordinates): 

2237 other = other._a 

2238 a = self._a 

2239 assert len(a) == len(other) 

2240 for i, value in enumerate(other): 

2241 a[i] -= value 

2242 return self 

2243 return NotImplemented 

2244 

2245 def __imul__(self, other): 

2246 """ 

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

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

2249 >>> g *= 2 

2250 >>> g 

2251 GlyphCoordinates([(4, 2)]) 

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

2253 >>> g *= 2 

2254 >>> g 

2255 GlyphCoordinates([(2, 4)]) 

2256 """ 

2257 if isinstance(other, tuple): 

2258 assert len(other) == 2 

2259 self.scale(other) 

2260 return self 

2261 if isinstance(other, Number): 

2262 if other == 1: 

2263 return self 

2264 a = self._a 

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

2266 a[i] *= other 

2267 return self 

2268 return NotImplemented 

2269 

2270 def __itruediv__(self, other): 

2271 """ 

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

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

2274 >>> g /= 2 

2275 >>> g 

2276 GlyphCoordinates([(1, 1)]) 

2277 """ 

2278 if isinstance(other, Number): 

2279 other = (other, other) 

2280 if isinstance(other, tuple): 

2281 if other == (1, 1): 

2282 return self 

2283 assert len(other) == 2 

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

2285 return self 

2286 return NotImplemented 

2287 

2288 def __bool__(self): 

2289 """ 

2290 >>> g = GlyphCoordinates([]) 

2291 >>> bool(g) 

2292 False 

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

2294 >>> bool(g) 

2295 True 

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

2297 >>> bool(g) 

2298 True 

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

2300 >>> bool(g) 

2301 True 

2302 """ 

2303 return bool(self._a) 

2304 

2305 __nonzero__ = __bool__ 

2306 

2307 

2308if __name__ == "__main__": 

2309 import doctest, sys 

2310 

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