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

1521 statements  

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

1# coding: utf-8 

2"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various 

3OpenType subtables. 

4 

5Most are constructed upon import from data in otData.py, all are populated with 

6converter objects from otConverters.py. 

7""" 

8import copy 

9from enum import IntEnum 

10from functools import reduce 

11from math import radians 

12import itertools 

13from collections import defaultdict, namedtuple 

14from fontTools.ttLib.tables.otTraverse import dfs_base_table 

15from fontTools.misc.arrayTools import quantizeRect 

16from fontTools.misc.roundTools import otRound 

17from fontTools.misc.transform import Transform, Identity 

18from fontTools.misc.textTools import bytesjoin, pad, safeEval 

19from fontTools.pens.boundsPen import ControlBoundsPen 

20from fontTools.pens.transformPen import TransformPen 

21from .otBase import ( 

22 BaseTable, 

23 FormatSwitchingBaseTable, 

24 ValueRecord, 

25 CountReference, 

26 getFormatSwitchingBaseTableClass, 

27) 

28from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY 

29import logging 

30import struct 

31from typing import TYPE_CHECKING, Iterator, List, Optional, Set 

32 

33if TYPE_CHECKING: 

34 from fontTools.ttLib.ttGlyphSet import _TTGlyphSet 

35 

36 

37log = logging.getLogger(__name__) 

38 

39 

40class AATStateTable(object): 

41 def __init__(self): 

42 self.GlyphClasses = {} # GlyphID --> GlyphClass 

43 self.States = [] # List of AATState, indexed by state number 

44 self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...] 

45 

46 

47class AATState(object): 

48 def __init__(self): 

49 self.Transitions = {} # GlyphClass --> AATAction 

50 

51 

52class AATAction(object): 

53 _FLAGS = None 

54 

55 @staticmethod 

56 def compileActions(font, states): 

57 return (None, None) 

58 

59 def _writeFlagsToXML(self, xmlWriter): 

60 flags = [f for f in self._FLAGS if self.__dict__[f]] 

61 if flags: 

62 xmlWriter.simpletag("Flags", value=",".join(flags)) 

63 xmlWriter.newline() 

64 if self.ReservedFlags != 0: 

65 xmlWriter.simpletag("ReservedFlags", value="0x%04X" % self.ReservedFlags) 

66 xmlWriter.newline() 

67 

68 def _setFlag(self, flag): 

69 assert flag in self._FLAGS, "unsupported flag %s" % flag 

70 self.__dict__[flag] = True 

71 

72 

73class RearrangementMorphAction(AATAction): 

74 staticSize = 4 

75 actionHeaderSize = 0 

76 _FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"] 

77 

78 _VERBS = { 

79 0: "no change", 

80 1: "Ax ⇒ xA", 

81 2: "xD ⇒ Dx", 

82 3: "AxD ⇒ DxA", 

83 4: "ABx ⇒ xAB", 

84 5: "ABx ⇒ xBA", 

85 6: "xCD ⇒ CDx", 

86 7: "xCD ⇒ DCx", 

87 8: "AxCD ⇒ CDxA", 

88 9: "AxCD ⇒ DCxA", 

89 10: "ABxD ⇒ DxAB", 

90 11: "ABxD ⇒ DxBA", 

91 12: "ABxCD ⇒ CDxAB", 

92 13: "ABxCD ⇒ CDxBA", 

93 14: "ABxCD ⇒ DCxAB", 

94 15: "ABxCD ⇒ DCxBA", 

95 } 

96 

97 def __init__(self): 

98 self.NewState = 0 

99 self.Verb = 0 

100 self.MarkFirst = False 

101 self.DontAdvance = False 

102 self.MarkLast = False 

103 self.ReservedFlags = 0 

104 

105 def compile(self, writer, font, actionIndex): 

106 assert actionIndex is None 

107 writer.writeUShort(self.NewState) 

108 assert self.Verb >= 0 and self.Verb <= 15, self.Verb 

109 flags = self.Verb | self.ReservedFlags 

110 if self.MarkFirst: 

111 flags |= 0x8000 

112 if self.DontAdvance: 

113 flags |= 0x4000 

114 if self.MarkLast: 

115 flags |= 0x2000 

116 writer.writeUShort(flags) 

117 

118 def decompile(self, reader, font, actionReader): 

119 assert actionReader is None 

120 self.NewState = reader.readUShort() 

121 flags = reader.readUShort() 

122 self.Verb = flags & 0xF 

123 self.MarkFirst = bool(flags & 0x8000) 

124 self.DontAdvance = bool(flags & 0x4000) 

125 self.MarkLast = bool(flags & 0x2000) 

126 self.ReservedFlags = flags & 0x1FF0 

127 

128 def toXML(self, xmlWriter, font, attrs, name): 

129 xmlWriter.begintag(name, **attrs) 

130 xmlWriter.newline() 

131 xmlWriter.simpletag("NewState", value=self.NewState) 

132 xmlWriter.newline() 

133 self._writeFlagsToXML(xmlWriter) 

134 xmlWriter.simpletag("Verb", value=self.Verb) 

135 verbComment = self._VERBS.get(self.Verb) 

136 if verbComment is not None: 

137 xmlWriter.comment(verbComment) 

138 xmlWriter.newline() 

139 xmlWriter.endtag(name) 

140 xmlWriter.newline() 

141 

142 def fromXML(self, name, attrs, content, font): 

143 self.NewState = self.Verb = self.ReservedFlags = 0 

144 self.MarkFirst = self.DontAdvance = self.MarkLast = False 

145 content = [t for t in content if isinstance(t, tuple)] 

146 for eltName, eltAttrs, eltContent in content: 

147 if eltName == "NewState": 

148 self.NewState = safeEval(eltAttrs["value"]) 

149 elif eltName == "Verb": 

150 self.Verb = safeEval(eltAttrs["value"]) 

151 elif eltName == "ReservedFlags": 

152 self.ReservedFlags = safeEval(eltAttrs["value"]) 

153 elif eltName == "Flags": 

154 for flag in eltAttrs["value"].split(","): 

155 self._setFlag(flag.strip()) 

156 

157 

158class ContextualMorphAction(AATAction): 

159 staticSize = 8 

160 actionHeaderSize = 0 

161 _FLAGS = ["SetMark", "DontAdvance"] 

162 

163 def __init__(self): 

164 self.NewState = 0 

165 self.SetMark, self.DontAdvance = False, False 

166 self.ReservedFlags = 0 

167 self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF 

168 

169 def compile(self, writer, font, actionIndex): 

170 assert actionIndex is None 

171 writer.writeUShort(self.NewState) 

172 flags = self.ReservedFlags 

173 if self.SetMark: 

174 flags |= 0x8000 

175 if self.DontAdvance: 

176 flags |= 0x4000 

177 writer.writeUShort(flags) 

178 writer.writeUShort(self.MarkIndex) 

179 writer.writeUShort(self.CurrentIndex) 

180 

181 def decompile(self, reader, font, actionReader): 

182 assert actionReader is None 

183 self.NewState = reader.readUShort() 

184 flags = reader.readUShort() 

185 self.SetMark = bool(flags & 0x8000) 

186 self.DontAdvance = bool(flags & 0x4000) 

187 self.ReservedFlags = flags & 0x3FFF 

188 self.MarkIndex = reader.readUShort() 

189 self.CurrentIndex = reader.readUShort() 

190 

191 def toXML(self, xmlWriter, font, attrs, name): 

192 xmlWriter.begintag(name, **attrs) 

193 xmlWriter.newline() 

194 xmlWriter.simpletag("NewState", value=self.NewState) 

195 xmlWriter.newline() 

196 self._writeFlagsToXML(xmlWriter) 

197 xmlWriter.simpletag("MarkIndex", value=self.MarkIndex) 

198 xmlWriter.newline() 

199 xmlWriter.simpletag("CurrentIndex", value=self.CurrentIndex) 

200 xmlWriter.newline() 

201 xmlWriter.endtag(name) 

202 xmlWriter.newline() 

203 

204 def fromXML(self, name, attrs, content, font): 

205 self.NewState = self.ReservedFlags = 0 

206 self.SetMark = self.DontAdvance = False 

207 self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF 

208 content = [t for t in content if isinstance(t, tuple)] 

209 for eltName, eltAttrs, eltContent in content: 

210 if eltName == "NewState": 

211 self.NewState = safeEval(eltAttrs["value"]) 

212 elif eltName == "Flags": 

213 for flag in eltAttrs["value"].split(","): 

214 self._setFlag(flag.strip()) 

215 elif eltName == "ReservedFlags": 

216 self.ReservedFlags = safeEval(eltAttrs["value"]) 

217 elif eltName == "MarkIndex": 

218 self.MarkIndex = safeEval(eltAttrs["value"]) 

219 elif eltName == "CurrentIndex": 

220 self.CurrentIndex = safeEval(eltAttrs["value"]) 

221 

222 

223class LigAction(object): 

224 def __init__(self): 

225 self.Store = False 

226 # GlyphIndexDelta is a (possibly negative) delta that gets 

227 # added to the glyph ID at the top of the AAT runtime 

228 # execution stack. It is *not* a byte offset into the 

229 # morx table. The result of the addition, which is performed 

230 # at run time by the shaping engine, is an index into 

231 # the ligature components table. See 'morx' specification. 

232 # In the AAT specification, this field is called Offset; 

233 # but its meaning is quite different from other offsets 

234 # in either AAT or OpenType, so we use a different name. 

235 self.GlyphIndexDelta = 0 

236 

237 

238class LigatureMorphAction(AATAction): 

239 staticSize = 6 

240 

241 # 4 bytes for each of {action,ligComponents,ligatures}Offset 

242 actionHeaderSize = 12 

243 

244 _FLAGS = ["SetComponent", "DontAdvance"] 

245 

246 def __init__(self): 

247 self.NewState = 0 

248 self.SetComponent, self.DontAdvance = False, False 

249 self.ReservedFlags = 0 

250 self.Actions = [] 

251 

252 def compile(self, writer, font, actionIndex): 

253 assert actionIndex is not None 

254 writer.writeUShort(self.NewState) 

255 flags = self.ReservedFlags 

256 if self.SetComponent: 

257 flags |= 0x8000 

258 if self.DontAdvance: 

259 flags |= 0x4000 

260 if len(self.Actions) > 0: 

261 flags |= 0x2000 

262 writer.writeUShort(flags) 

263 if len(self.Actions) > 0: 

264 actions = self.compileLigActions() 

265 writer.writeUShort(actionIndex[actions]) 

266 else: 

267 writer.writeUShort(0) 

268 

269 def decompile(self, reader, font, actionReader): 

270 assert actionReader is not None 

271 self.NewState = reader.readUShort() 

272 flags = reader.readUShort() 

273 self.SetComponent = bool(flags & 0x8000) 

274 self.DontAdvance = bool(flags & 0x4000) 

275 performAction = bool(flags & 0x2000) 

276 # As of 2017-09-12, the 'morx' specification says that 

277 # the reserved bitmask in ligature subtables is 0x3FFF. 

278 # However, the specification also defines a flag 0x2000, 

279 # so the reserved value should actually be 0x1FFF. 

280 # TODO: Report this specification bug to Apple. 

281 self.ReservedFlags = flags & 0x1FFF 

282 actionIndex = reader.readUShort() 

283 if performAction: 

284 self.Actions = self._decompileLigActions(actionReader, actionIndex) 

285 else: 

286 self.Actions = [] 

287 

288 @staticmethod 

289 def compileActions(font, states): 

290 result, actions, actionIndex = b"", set(), {} 

291 for state in states: 

292 for _glyphClass, trans in state.Transitions.items(): 

293 actions.add(trans.compileLigActions()) 

294 # Sort the compiled actions in decreasing order of 

295 # length, so that the longer sequence come before the 

296 # shorter ones. For each compiled action ABCD, its 

297 # suffixes BCD, CD, and D do not be encoded separately 

298 # (in case they occur); instead, we can just store an 

299 # index that points into the middle of the longer 

300 # sequence. Every compiled AAT ligature sequence is 

301 # terminated with an end-of-sequence flag, which can 

302 # only be set on the last element of the sequence. 

303 # Therefore, it is sufficient to consider just the 

304 # suffixes. 

305 for a in sorted(actions, key=lambda x: (-len(x), x)): 

306 if a not in actionIndex: 

307 for i in range(0, len(a), 4): 

308 suffix = a[i:] 

309 suffixIndex = (len(result) + i) // 4 

310 actionIndex.setdefault(suffix, suffixIndex) 

311 result += a 

312 result = pad(result, 4) 

313 return (result, actionIndex) 

314 

315 def compileLigActions(self): 

316 result = [] 

317 for i, action in enumerate(self.Actions): 

318 last = i == len(self.Actions) - 1 

319 value = action.GlyphIndexDelta & 0x3FFFFFFF 

320 value |= 0x80000000 if last else 0 

321 value |= 0x40000000 if action.Store else 0 

322 result.append(struct.pack(">L", value)) 

323 return bytesjoin(result) 

324 

325 def _decompileLigActions(self, actionReader, actionIndex): 

326 actions = [] 

327 last = False 

328 reader = actionReader.getSubReader(actionReader.pos + actionIndex * 4) 

329 while not last: 

330 value = reader.readULong() 

331 last = bool(value & 0x80000000) 

332 action = LigAction() 

333 actions.append(action) 

334 action.Store = bool(value & 0x40000000) 

335 delta = value & 0x3FFFFFFF 

336 if delta >= 0x20000000: # sign-extend 30-bit value 

337 delta = -0x40000000 + delta 

338 action.GlyphIndexDelta = delta 

339 return actions 

340 

341 def fromXML(self, name, attrs, content, font): 

342 self.NewState = self.ReservedFlags = 0 

343 self.SetComponent = self.DontAdvance = False 

344 self.ReservedFlags = 0 

345 self.Actions = [] 

346 content = [t for t in content if isinstance(t, tuple)] 

347 for eltName, eltAttrs, eltContent in content: 

348 if eltName == "NewState": 

349 self.NewState = safeEval(eltAttrs["value"]) 

350 elif eltName == "Flags": 

351 for flag in eltAttrs["value"].split(","): 

352 self._setFlag(flag.strip()) 

353 elif eltName == "ReservedFlags": 

354 self.ReservedFlags = safeEval(eltAttrs["value"]) 

355 elif eltName == "Action": 

356 action = LigAction() 

357 flags = eltAttrs.get("Flags", "").split(",") 

358 flags = [f.strip() for f in flags] 

359 action.Store = "Store" in flags 

360 action.GlyphIndexDelta = safeEval(eltAttrs["GlyphIndexDelta"]) 

361 self.Actions.append(action) 

362 

363 def toXML(self, xmlWriter, font, attrs, name): 

364 xmlWriter.begintag(name, **attrs) 

365 xmlWriter.newline() 

366 xmlWriter.simpletag("NewState", value=self.NewState) 

367 xmlWriter.newline() 

368 self._writeFlagsToXML(xmlWriter) 

369 for action in self.Actions: 

370 attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)] 

371 if action.Store: 

372 attribs.append(("Flags", "Store")) 

373 xmlWriter.simpletag("Action", attribs) 

374 xmlWriter.newline() 

375 xmlWriter.endtag(name) 

376 xmlWriter.newline() 

377 

378 

379class InsertionMorphAction(AATAction): 

380 staticSize = 8 

381 actionHeaderSize = 4 # 4 bytes for actionOffset 

382 _FLAGS = [ 

383 "SetMark", 

384 "DontAdvance", 

385 "CurrentIsKashidaLike", 

386 "MarkedIsKashidaLike", 

387 "CurrentInsertBefore", 

388 "MarkedInsertBefore", 

389 ] 

390 

391 def __init__(self): 

392 self.NewState = 0 

393 for flag in self._FLAGS: 

394 setattr(self, flag, False) 

395 self.ReservedFlags = 0 

396 self.CurrentInsertionAction, self.MarkedInsertionAction = [], [] 

397 

398 def compile(self, writer, font, actionIndex): 

399 assert actionIndex is not None 

400 writer.writeUShort(self.NewState) 

401 flags = self.ReservedFlags 

402 if self.SetMark: 

403 flags |= 0x8000 

404 if self.DontAdvance: 

405 flags |= 0x4000 

406 if self.CurrentIsKashidaLike: 

407 flags |= 0x2000 

408 if self.MarkedIsKashidaLike: 

409 flags |= 0x1000 

410 if self.CurrentInsertBefore: 

411 flags |= 0x0800 

412 if self.MarkedInsertBefore: 

413 flags |= 0x0400 

414 flags |= len(self.CurrentInsertionAction) << 5 

415 flags |= len(self.MarkedInsertionAction) 

416 writer.writeUShort(flags) 

417 if len(self.CurrentInsertionAction) > 0: 

418 currentIndex = actionIndex[tuple(self.CurrentInsertionAction)] 

419 else: 

420 currentIndex = 0xFFFF 

421 writer.writeUShort(currentIndex) 

422 if len(self.MarkedInsertionAction) > 0: 

423 markedIndex = actionIndex[tuple(self.MarkedInsertionAction)] 

424 else: 

425 markedIndex = 0xFFFF 

426 writer.writeUShort(markedIndex) 

427 

428 def decompile(self, reader, font, actionReader): 

429 assert actionReader is not None 

430 self.NewState = reader.readUShort() 

431 flags = reader.readUShort() 

432 self.SetMark = bool(flags & 0x8000) 

433 self.DontAdvance = bool(flags & 0x4000) 

434 self.CurrentIsKashidaLike = bool(flags & 0x2000) 

435 self.MarkedIsKashidaLike = bool(flags & 0x1000) 

436 self.CurrentInsertBefore = bool(flags & 0x0800) 

437 self.MarkedInsertBefore = bool(flags & 0x0400) 

438 self.CurrentInsertionAction = self._decompileInsertionAction( 

439 actionReader, font, index=reader.readUShort(), count=((flags & 0x03E0) >> 5) 

440 ) 

441 self.MarkedInsertionAction = self._decompileInsertionAction( 

442 actionReader, font, index=reader.readUShort(), count=(flags & 0x001F) 

443 ) 

444 

445 def _decompileInsertionAction(self, actionReader, font, index, count): 

446 if index == 0xFFFF or count == 0: 

447 return [] 

448 reader = actionReader.getSubReader(actionReader.pos + index * 2) 

449 return font.getGlyphNameMany(reader.readUShortArray(count)) 

450 

451 def toXML(self, xmlWriter, font, attrs, name): 

452 xmlWriter.begintag(name, **attrs) 

453 xmlWriter.newline() 

454 xmlWriter.simpletag("NewState", value=self.NewState) 

455 xmlWriter.newline() 

456 self._writeFlagsToXML(xmlWriter) 

457 for g in self.CurrentInsertionAction: 

458 xmlWriter.simpletag("CurrentInsertionAction", glyph=g) 

459 xmlWriter.newline() 

460 for g in self.MarkedInsertionAction: 

461 xmlWriter.simpletag("MarkedInsertionAction", glyph=g) 

462 xmlWriter.newline() 

463 xmlWriter.endtag(name) 

464 xmlWriter.newline() 

465 

466 def fromXML(self, name, attrs, content, font): 

467 self.__init__() 

468 content = [t for t in content if isinstance(t, tuple)] 

469 for eltName, eltAttrs, eltContent in content: 

470 if eltName == "NewState": 

471 self.NewState = safeEval(eltAttrs["value"]) 

472 elif eltName == "Flags": 

473 for flag in eltAttrs["value"].split(","): 

474 self._setFlag(flag.strip()) 

475 elif eltName == "CurrentInsertionAction": 

476 self.CurrentInsertionAction.append(eltAttrs["glyph"]) 

477 elif eltName == "MarkedInsertionAction": 

478 self.MarkedInsertionAction.append(eltAttrs["glyph"]) 

479 else: 

480 assert False, eltName 

481 

482 @staticmethod 

483 def compileActions(font, states): 

484 actions, actionIndex, result = set(), {}, b"" 

485 for state in states: 

486 for _glyphClass, trans in state.Transitions.items(): 

487 if trans.CurrentInsertionAction is not None: 

488 actions.add(tuple(trans.CurrentInsertionAction)) 

489 if trans.MarkedInsertionAction is not None: 

490 actions.add(tuple(trans.MarkedInsertionAction)) 

491 # Sort the compiled actions in decreasing order of 

492 # length, so that the longer sequence come before the 

493 # shorter ones. 

494 for action in sorted(actions, key=lambda x: (-len(x), x)): 

495 # We insert all sub-sequences of the action glyph sequence 

496 # into actionIndex. For example, if one action triggers on 

497 # glyph sequence [A, B, C, D, E] and another action triggers 

498 # on [C, D], we return result=[A, B, C, D, E] (as list of 

499 # encoded glyph IDs), and actionIndex={('A','B','C','D','E'): 0, 

500 # ('C','D'): 2}. 

501 if action in actionIndex: 

502 continue 

503 for start in range(0, len(action)): 

504 startIndex = (len(result) // 2) + start 

505 for limit in range(start, len(action)): 

506 glyphs = action[start : limit + 1] 

507 actionIndex.setdefault(glyphs, startIndex) 

508 for glyph in action: 

509 glyphID = font.getGlyphID(glyph) 

510 result += struct.pack(">H", glyphID) 

511 return result, actionIndex 

512 

513 

514class FeatureParams(BaseTable): 

515 def compile(self, writer, font): 

516 assert ( 

517 featureParamTypes.get(writer["FeatureTag"]) == self.__class__ 

518 ), "Wrong FeatureParams type for feature '%s': %s" % ( 

519 writer["FeatureTag"], 

520 self.__class__.__name__, 

521 ) 

522 BaseTable.compile(self, writer, font) 

523 

524 def toXML(self, xmlWriter, font, attrs=None, name=None): 

525 BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__) 

526 

527 

528class FeatureParamsSize(FeatureParams): 

529 pass 

530 

531 

532class FeatureParamsStylisticSet(FeatureParams): 

533 pass 

534 

535 

536class FeatureParamsCharacterVariants(FeatureParams): 

537 pass 

538 

539 

540class Coverage(FormatSwitchingBaseTable): 

541 # manual implementation to get rid of glyphID dependencies 

542 

543 def populateDefaults(self, propagator=None): 

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

545 self.glyphs = [] 

546 

547 def postRead(self, rawTable, font): 

548 if self.Format == 1: 

549 self.glyphs = rawTable["GlyphArray"] 

550 elif self.Format == 2: 

551 glyphs = self.glyphs = [] 

552 ranges = rawTable["RangeRecord"] 

553 # Some SIL fonts have coverage entries that don't have sorted 

554 # StartCoverageIndex. If it is so, fixup and warn. We undo 

555 # this when writing font out. 

556 sorted_ranges = sorted(ranges, key=lambda a: a.StartCoverageIndex) 

557 if ranges != sorted_ranges: 

558 log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.") 

559 ranges = sorted_ranges 

560 del sorted_ranges 

561 for r in ranges: 

562 start = r.Start 

563 end = r.End 

564 startID = font.getGlyphID(start) 

565 endID = font.getGlyphID(end) + 1 

566 glyphs.extend(font.getGlyphNameMany(range(startID, endID))) 

567 else: 

568 self.glyphs = [] 

569 log.warning("Unknown Coverage format: %s", self.Format) 

570 del self.Format # Don't need this anymore 

571 

572 def preWrite(self, font): 

573 glyphs = getattr(self, "glyphs", None) 

574 if glyphs is None: 

575 glyphs = self.glyphs = [] 

576 format = 1 

577 rawTable = {"GlyphArray": glyphs} 

578 if glyphs: 

579 # find out whether Format 2 is more compact or not 

580 glyphIDs = font.getGlyphIDMany(glyphs) 

581 brokenOrder = sorted(glyphIDs) != glyphIDs 

582 

583 last = glyphIDs[0] 

584 ranges = [[last]] 

585 for glyphID in glyphIDs[1:]: 

586 if glyphID != last + 1: 

587 ranges[-1].append(last) 

588 ranges.append([glyphID]) 

589 last = glyphID 

590 ranges[-1].append(last) 

591 

592 if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word 

593 # Format 2 is more compact 

594 index = 0 

595 for i in range(len(ranges)): 

596 start, end = ranges[i] 

597 r = RangeRecord() 

598 r.StartID = start 

599 r.Start = font.getGlyphName(start) 

600 r.End = font.getGlyphName(end) 

601 r.StartCoverageIndex = index 

602 ranges[i] = r 

603 index = index + end - start + 1 

604 if brokenOrder: 

605 log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.") 

606 ranges.sort(key=lambda a: a.StartID) 

607 for r in ranges: 

608 del r.StartID 

609 format = 2 

610 rawTable = {"RangeRecord": ranges} 

611 # else: 

612 # fallthrough; Format 1 is more compact 

613 self.Format = format 

614 return rawTable 

615 

616 def toXML2(self, xmlWriter, font): 

617 for glyphName in getattr(self, "glyphs", []): 

618 xmlWriter.simpletag("Glyph", value=glyphName) 

619 xmlWriter.newline() 

620 

621 def fromXML(self, name, attrs, content, font): 

622 glyphs = getattr(self, "glyphs", None) 

623 if glyphs is None: 

624 glyphs = [] 

625 self.glyphs = glyphs 

626 glyphs.append(attrs["value"]) 

627 

628 

629# The special 0xFFFFFFFF delta-set index is used to indicate that there 

630# is no variation data in the ItemVariationStore for a given variable field 

631NO_VARIATION_INDEX = 0xFFFFFFFF 

632 

633 

634class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")): 

635 def populateDefaults(self, propagator=None): 

636 if not hasattr(self, "mapping"): 

637 self.mapping = [] 

638 

639 def postRead(self, rawTable, font): 

640 assert (rawTable["EntryFormat"] & 0xFFC0) == 0 

641 self.mapping = rawTable["mapping"] 

642 

643 @staticmethod 

644 def getEntryFormat(mapping): 

645 ored = 0 

646 for idx in mapping: 

647 ored |= idx 

648 

649 inner = ored & 0xFFFF 

650 innerBits = 0 

651 while inner: 

652 innerBits += 1 

653 inner >>= 1 

654 innerBits = max(innerBits, 1) 

655 assert innerBits <= 16 

656 

657 ored = (ored >> (16 - innerBits)) | (ored & ((1 << innerBits) - 1)) 

658 if ored <= 0x000000FF: 

659 entrySize = 1 

660 elif ored <= 0x0000FFFF: 

661 entrySize = 2 

662 elif ored <= 0x00FFFFFF: 

663 entrySize = 3 

664 else: 

665 entrySize = 4 

666 

667 return ((entrySize - 1) << 4) | (innerBits - 1) 

668 

669 def preWrite(self, font): 

670 mapping = getattr(self, "mapping", None) 

671 if mapping is None: 

672 mapping = self.mapping = [] 

673 self.Format = 1 if len(mapping) > 0xFFFF else 0 

674 rawTable = self.__dict__.copy() 

675 rawTable["MappingCount"] = len(mapping) 

676 rawTable["EntryFormat"] = self.getEntryFormat(mapping) 

677 return rawTable 

678 

679 def toXML2(self, xmlWriter, font): 

680 # Make xml dump less verbose, by omitting no-op entries like: 

681 # <Map index="..." outer="65535" inner="65535"/> 

682 xmlWriter.comment("Omitted values default to 0xFFFF/0xFFFF (no variations)") 

683 xmlWriter.newline() 

684 for i, value in enumerate(getattr(self, "mapping", [])): 

685 attrs = [("index", i)] 

686 if value != NO_VARIATION_INDEX: 

687 attrs.extend( 

688 [ 

689 ("outer", value >> 16), 

690 ("inner", value & 0xFFFF), 

691 ] 

692 ) 

693 xmlWriter.simpletag("Map", attrs) 

694 xmlWriter.newline() 

695 

696 def fromXML(self, name, attrs, content, font): 

697 mapping = getattr(self, "mapping", None) 

698 if mapping is None: 

699 self.mapping = mapping = [] 

700 index = safeEval(attrs["index"]) 

701 outer = safeEval(attrs.get("outer", "0xFFFF")) 

702 inner = safeEval(attrs.get("inner", "0xFFFF")) 

703 assert inner <= 0xFFFF 

704 mapping.insert(index, (outer << 16) | inner) 

705 

706 

707class VarIdxMap(BaseTable): 

708 def populateDefaults(self, propagator=None): 

709 if not hasattr(self, "mapping"): 

710 self.mapping = {} 

711 

712 def postRead(self, rawTable, font): 

713 assert (rawTable["EntryFormat"] & 0xFFC0) == 0 

714 glyphOrder = font.getGlyphOrder() 

715 mapList = rawTable["mapping"] 

716 mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList))) 

717 self.mapping = dict(zip(glyphOrder, mapList)) 

718 

719 def preWrite(self, font): 

720 mapping = getattr(self, "mapping", None) 

721 if mapping is None: 

722 mapping = self.mapping = {} 

723 

724 glyphOrder = font.getGlyphOrder() 

725 mapping = [mapping[g] for g in glyphOrder] 

726 while len(mapping) > 1 and mapping[-2] == mapping[-1]: 

727 del mapping[-1] 

728 

729 rawTable = {"mapping": mapping} 

730 rawTable["MappingCount"] = len(mapping) 

731 rawTable["EntryFormat"] = DeltaSetIndexMap.getEntryFormat(mapping) 

732 return rawTable 

733 

734 def toXML2(self, xmlWriter, font): 

735 for glyph, value in sorted(getattr(self, "mapping", {}).items()): 

736 attrs = ( 

737 ("glyph", glyph), 

738 ("outer", value >> 16), 

739 ("inner", value & 0xFFFF), 

740 ) 

741 xmlWriter.simpletag("Map", attrs) 

742 xmlWriter.newline() 

743 

744 def fromXML(self, name, attrs, content, font): 

745 mapping = getattr(self, "mapping", None) 

746 if mapping is None: 

747 mapping = {} 

748 self.mapping = mapping 

749 try: 

750 glyph = attrs["glyph"] 

751 except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836 

752 glyph = font.getGlyphOrder()[attrs["index"]] 

753 outer = safeEval(attrs["outer"]) 

754 inner = safeEval(attrs["inner"]) 

755 assert inner <= 0xFFFF 

756 mapping[glyph] = (outer << 16) | inner 

757 

758 

759class VarRegionList(BaseTable): 

760 def preWrite(self, font): 

761 # The OT spec says VarStore.VarRegionList.RegionAxisCount should always 

762 # be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule 

763 # even when the VarRegionList is empty. We can't treat RegionAxisCount 

764 # like a normal propagated count (== len(Region[i].VarRegionAxis)), 

765 # otherwise it would default to 0 if VarRegionList is empty. 

766 # Thus, we force it to always be equal to fvar.axisCount. 

767 # https://github.com/khaledhosny/ots/pull/192 

768 fvarTable = font.get("fvar") 

769 if fvarTable: 

770 self.RegionAxisCount = len(fvarTable.axes) 

771 return { 

772 **self.__dict__, 

773 "RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount"), 

774 } 

775 

776 

777class SingleSubst(FormatSwitchingBaseTable): 

778 def populateDefaults(self, propagator=None): 

779 if not hasattr(self, "mapping"): 

780 self.mapping = {} 

781 

782 def postRead(self, rawTable, font): 

783 mapping = {} 

784 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 

785 if self.Format == 1: 

786 delta = rawTable["DeltaGlyphID"] 

787 inputGIDS = font.getGlyphIDMany(input) 

788 outGIDS = [(glyphID + delta) % 65536 for glyphID in inputGIDS] 

789 outNames = font.getGlyphNameMany(outGIDS) 

790 for inp, out in zip(input, outNames): 

791 mapping[inp] = out 

792 elif self.Format == 2: 

793 assert ( 

794 len(input) == rawTable["GlyphCount"] 

795 ), "invalid SingleSubstFormat2 table" 

796 subst = rawTable["Substitute"] 

797 for inp, sub in zip(input, subst): 

798 mapping[inp] = sub 

799 else: 

800 assert 0, "unknown format: %s" % self.Format 

801 self.mapping = mapping 

802 del self.Format # Don't need this anymore 

803 

804 def preWrite(self, font): 

805 mapping = getattr(self, "mapping", None) 

806 if mapping is None: 

807 mapping = self.mapping = {} 

808 items = list(mapping.items()) 

809 getGlyphID = font.getGlyphID 

810 gidItems = [(getGlyphID(a), getGlyphID(b)) for a, b in items] 

811 sortableItems = sorted(zip(gidItems, items)) 

812 

813 # figure out format 

814 format = 2 

815 delta = None 

816 for inID, outID in gidItems: 

817 if delta is None: 

818 delta = (outID - inID) % 65536 

819 

820 if (inID + delta) % 65536 != outID: 

821 break 

822 else: 

823 if delta is None: 

824 # the mapping is empty, better use format 2 

825 format = 2 

826 else: 

827 format = 1 

828 

829 rawTable = {} 

830 self.Format = format 

831 cov = Coverage() 

832 input = [item[1][0] for item in sortableItems] 

833 subst = [item[1][1] for item in sortableItems] 

834 cov.glyphs = input 

835 rawTable["Coverage"] = cov 

836 if format == 1: 

837 assert delta is not None 

838 rawTable["DeltaGlyphID"] = delta 

839 else: 

840 rawTable["Substitute"] = subst 

841 return rawTable 

842 

843 def toXML2(self, xmlWriter, font): 

844 items = sorted(self.mapping.items()) 

845 for inGlyph, outGlyph in items: 

846 xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)]) 

847 xmlWriter.newline() 

848 

849 def fromXML(self, name, attrs, content, font): 

850 mapping = getattr(self, "mapping", None) 

851 if mapping is None: 

852 mapping = {} 

853 self.mapping = mapping 

854 mapping[attrs["in"]] = attrs["out"] 

855 

856 

857class MultipleSubst(FormatSwitchingBaseTable): 

858 def populateDefaults(self, propagator=None): 

859 if not hasattr(self, "mapping"): 

860 self.mapping = {} 

861 

862 def postRead(self, rawTable, font): 

863 mapping = {} 

864 if self.Format == 1: 

865 glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 

866 subst = [s.Substitute for s in rawTable["Sequence"]] 

867 mapping = dict(zip(glyphs, subst)) 

868 else: 

869 assert 0, "unknown format: %s" % self.Format 

870 self.mapping = mapping 

871 del self.Format # Don't need this anymore 

872 

873 def preWrite(self, font): 

874 mapping = getattr(self, "mapping", None) 

875 if mapping is None: 

876 mapping = self.mapping = {} 

877 cov = Coverage() 

878 cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID) 

879 self.Format = 1 

880 rawTable = { 

881 "Coverage": cov, 

882 "Sequence": [self.makeSequence_(mapping[glyph]) for glyph in cov.glyphs], 

883 } 

884 return rawTable 

885 

886 def toXML2(self, xmlWriter, font): 

887 items = sorted(self.mapping.items()) 

888 for inGlyph, outGlyphs in items: 

889 out = ",".join(outGlyphs) 

890 xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", out)]) 

891 xmlWriter.newline() 

892 

893 def fromXML(self, name, attrs, content, font): 

894 mapping = getattr(self, "mapping", None) 

895 if mapping is None: 

896 mapping = {} 

897 self.mapping = mapping 

898 

899 # TTX v3.0 and earlier. 

900 if name == "Coverage": 

901 self.old_coverage_ = [] 

902 for element in content: 

903 if not isinstance(element, tuple): 

904 continue 

905 element_name, element_attrs, _ = element 

906 if element_name == "Glyph": 

907 self.old_coverage_.append(element_attrs["value"]) 

908 return 

909 if name == "Sequence": 

910 index = int(attrs.get("index", len(mapping))) 

911 glyph = self.old_coverage_[index] 

912 glyph_mapping = mapping[glyph] = [] 

913 for element in content: 

914 if not isinstance(element, tuple): 

915 continue 

916 element_name, element_attrs, _ = element 

917 if element_name == "Substitute": 

918 glyph_mapping.append(element_attrs["value"]) 

919 return 

920 

921 # TTX v3.1 and later. 

922 outGlyphs = attrs["out"].split(",") if attrs["out"] else [] 

923 mapping[attrs["in"]] = [g.strip() for g in outGlyphs] 

924 

925 @staticmethod 

926 def makeSequence_(g): 

927 seq = Sequence() 

928 seq.Substitute = g 

929 return seq 

930 

931 

932class ClassDef(FormatSwitchingBaseTable): 

933 def populateDefaults(self, propagator=None): 

934 if not hasattr(self, "classDefs"): 

935 self.classDefs = {} 

936 

937 def postRead(self, rawTable, font): 

938 classDefs = {} 

939 

940 if self.Format == 1: 

941 start = rawTable["StartGlyph"] 

942 classList = rawTable["ClassValueArray"] 

943 startID = font.getGlyphID(start) 

944 endID = startID + len(classList) 

945 glyphNames = font.getGlyphNameMany(range(startID, endID)) 

946 for glyphName, cls in zip(glyphNames, classList): 

947 if cls: 

948 classDefs[glyphName] = cls 

949 

950 elif self.Format == 2: 

951 records = rawTable["ClassRangeRecord"] 

952 for rec in records: 

953 cls = rec.Class 

954 if not cls: 

955 continue 

956 start = rec.Start 

957 end = rec.End 

958 startID = font.getGlyphID(start) 

959 endID = font.getGlyphID(end) + 1 

960 glyphNames = font.getGlyphNameMany(range(startID, endID)) 

961 for glyphName in glyphNames: 

962 classDefs[glyphName] = cls 

963 else: 

964 log.warning("Unknown ClassDef format: %s", self.Format) 

965 self.classDefs = classDefs 

966 del self.Format # Don't need this anymore 

967 

968 def _getClassRanges(self, font): 

969 classDefs = getattr(self, "classDefs", None) 

970 if classDefs is None: 

971 self.classDefs = {} 

972 return 

973 getGlyphID = font.getGlyphID 

974 items = [] 

975 for glyphName, cls in classDefs.items(): 

976 if not cls: 

977 continue 

978 items.append((getGlyphID(glyphName), glyphName, cls)) 

979 if items: 

980 items.sort() 

981 last, lastName, lastCls = items[0] 

982 ranges = [[lastCls, last, lastName]] 

983 for glyphID, glyphName, cls in items[1:]: 

984 if glyphID != last + 1 or cls != lastCls: 

985 ranges[-1].extend([last, lastName]) 

986 ranges.append([cls, glyphID, glyphName]) 

987 last = glyphID 

988 lastName = glyphName 

989 lastCls = cls 

990 ranges[-1].extend([last, lastName]) 

991 return ranges 

992 

993 def preWrite(self, font): 

994 format = 2 

995 rawTable = {"ClassRangeRecord": []} 

996 ranges = self._getClassRanges(font) 

997 if ranges: 

998 startGlyph = ranges[0][1] 

999 endGlyph = ranges[-1][3] 

1000 glyphCount = endGlyph - startGlyph + 1 

1001 if len(ranges) * 3 < glyphCount + 1: 

1002 # Format 2 is more compact 

1003 for i in range(len(ranges)): 

1004 cls, start, startName, end, endName = ranges[i] 

1005 rec = ClassRangeRecord() 

1006 rec.Start = startName 

1007 rec.End = endName 

1008 rec.Class = cls 

1009 ranges[i] = rec 

1010 format = 2 

1011 rawTable = {"ClassRangeRecord": ranges} 

1012 else: 

1013 # Format 1 is more compact 

1014 startGlyphName = ranges[0][2] 

1015 classes = [0] * glyphCount 

1016 for cls, start, startName, end, endName in ranges: 

1017 for g in range(start - startGlyph, end - startGlyph + 1): 

1018 classes[g] = cls 

1019 format = 1 

1020 rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes} 

1021 self.Format = format 

1022 return rawTable 

1023 

1024 def toXML2(self, xmlWriter, font): 

1025 items = sorted(self.classDefs.items()) 

1026 for glyphName, cls in items: 

1027 xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)]) 

1028 xmlWriter.newline() 

1029 

1030 def fromXML(self, name, attrs, content, font): 

1031 classDefs = getattr(self, "classDefs", None) 

1032 if classDefs is None: 

1033 classDefs = {} 

1034 self.classDefs = classDefs 

1035 classDefs[attrs["glyph"]] = int(attrs["class"]) 

1036 

1037 

1038class AlternateSubst(FormatSwitchingBaseTable): 

1039 def populateDefaults(self, propagator=None): 

1040 if not hasattr(self, "alternates"): 

1041 self.alternates = {} 

1042 

1043 def postRead(self, rawTable, font): 

1044 alternates = {} 

1045 if self.Format == 1: 

1046 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 

1047 alts = rawTable["AlternateSet"] 

1048 assert len(input) == len(alts) 

1049 for inp, alt in zip(input, alts): 

1050 alternates[inp] = alt.Alternate 

1051 else: 

1052 assert 0, "unknown format: %s" % self.Format 

1053 self.alternates = alternates 

1054 del self.Format # Don't need this anymore 

1055 

1056 def preWrite(self, font): 

1057 self.Format = 1 

1058 alternates = getattr(self, "alternates", None) 

1059 if alternates is None: 

1060 alternates = self.alternates = {} 

1061 items = list(alternates.items()) 

1062 for i in range(len(items)): 

1063 glyphName, set = items[i] 

1064 items[i] = font.getGlyphID(glyphName), glyphName, set 

1065 items.sort() 

1066 cov = Coverage() 

1067 cov.glyphs = [item[1] for item in items] 

1068 alternates = [] 

1069 setList = [item[-1] for item in items] 

1070 for set in setList: 

1071 alts = AlternateSet() 

1072 alts.Alternate = set 

1073 alternates.append(alts) 

1074 # a special case to deal with the fact that several hundred Adobe Japan1-5 

1075 # CJK fonts will overflow an offset if the coverage table isn't pushed to the end. 

1076 # Also useful in that when splitting a sub-table because of an offset overflow 

1077 # I don't need to calculate the change in the subtable offset due to the change in the coverage table size. 

1078 # Allows packing more rules in subtable. 

1079 self.sortCoverageLast = 1 

1080 return {"Coverage": cov, "AlternateSet": alternates} 

1081 

1082 def toXML2(self, xmlWriter, font): 

1083 items = sorted(self.alternates.items()) 

1084 for glyphName, alternates in items: 

1085 xmlWriter.begintag("AlternateSet", glyph=glyphName) 

1086 xmlWriter.newline() 

1087 for alt in alternates: 

1088 xmlWriter.simpletag("Alternate", glyph=alt) 

1089 xmlWriter.newline() 

1090 xmlWriter.endtag("AlternateSet") 

1091 xmlWriter.newline() 

1092 

1093 def fromXML(self, name, attrs, content, font): 

1094 alternates = getattr(self, "alternates", None) 

1095 if alternates is None: 

1096 alternates = {} 

1097 self.alternates = alternates 

1098 glyphName = attrs["glyph"] 

1099 set = [] 

1100 alternates[glyphName] = set 

1101 for element in content: 

1102 if not isinstance(element, tuple): 

1103 continue 

1104 name, attrs, content = element 

1105 set.append(attrs["glyph"]) 

1106 

1107 

1108class LigatureSubst(FormatSwitchingBaseTable): 

1109 def populateDefaults(self, propagator=None): 

1110 if not hasattr(self, "ligatures"): 

1111 self.ligatures = {} 

1112 

1113 def postRead(self, rawTable, font): 

1114 ligatures = {} 

1115 if self.Format == 1: 

1116 input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) 

1117 ligSets = rawTable["LigatureSet"] 

1118 assert len(input) == len(ligSets) 

1119 for i in range(len(input)): 

1120 ligatures[input[i]] = ligSets[i].Ligature 

1121 else: 

1122 assert 0, "unknown format: %s" % self.Format 

1123 self.ligatures = ligatures 

1124 del self.Format # Don't need this anymore 

1125 

1126 def preWrite(self, font): 

1127 self.Format = 1 

1128 ligatures = getattr(self, "ligatures", None) 

1129 if ligatures is None: 

1130 ligatures = self.ligatures = {} 

1131 

1132 if ligatures and isinstance(next(iter(ligatures)), tuple): 

1133 # New high-level API in v3.1 and later. Note that we just support compiling this 

1134 # for now. We don't load to this API, and don't do XML with it. 

1135 

1136 # ligatures is map from components-sequence to lig-glyph 

1137 newLigatures = dict() 

1138 for comps, lig in sorted( 

1139 ligatures.items(), key=lambda item: (-len(item[0]), item[0]) 

1140 ): 

1141 ligature = Ligature() 

1142 ligature.Component = comps[1:] 

1143 ligature.CompCount = len(comps) 

1144 ligature.LigGlyph = lig 

1145 newLigatures.setdefault(comps[0], []).append(ligature) 

1146 ligatures = newLigatures 

1147 

1148 items = list(ligatures.items()) 

1149 for i in range(len(items)): 

1150 glyphName, set = items[i] 

1151 items[i] = font.getGlyphID(glyphName), glyphName, set 

1152 items.sort() 

1153 cov = Coverage() 

1154 cov.glyphs = [item[1] for item in items] 

1155 

1156 ligSets = [] 

1157 setList = [item[-1] for item in items] 

1158 for set in setList: 

1159 ligSet = LigatureSet() 

1160 ligs = ligSet.Ligature = [] 

1161 for lig in set: 

1162 ligs.append(lig) 

1163 ligSets.append(ligSet) 

1164 # Useful in that when splitting a sub-table because of an offset overflow 

1165 # I don't need to calculate the change in subtabl offset due to the coverage table size. 

1166 # Allows packing more rules in subtable. 

1167 self.sortCoverageLast = 1 

1168 return {"Coverage": cov, "LigatureSet": ligSets} 

1169 

1170 def toXML2(self, xmlWriter, font): 

1171 items = sorted(self.ligatures.items()) 

1172 for glyphName, ligSets in items: 

1173 xmlWriter.begintag("LigatureSet", glyph=glyphName) 

1174 xmlWriter.newline() 

1175 for lig in ligSets: 

1176 xmlWriter.simpletag( 

1177 "Ligature", glyph=lig.LigGlyph, components=",".join(lig.Component) 

1178 ) 

1179 xmlWriter.newline() 

1180 xmlWriter.endtag("LigatureSet") 

1181 xmlWriter.newline() 

1182 

1183 def fromXML(self, name, attrs, content, font): 

1184 ligatures = getattr(self, "ligatures", None) 

1185 if ligatures is None: 

1186 ligatures = {} 

1187 self.ligatures = ligatures 

1188 glyphName = attrs["glyph"] 

1189 ligs = [] 

1190 ligatures[glyphName] = ligs 

1191 for element in content: 

1192 if not isinstance(element, tuple): 

1193 continue 

1194 name, attrs, content = element 

1195 lig = Ligature() 

1196 lig.LigGlyph = attrs["glyph"] 

1197 components = attrs["components"] 

1198 lig.Component = components.split(",") if components else [] 

1199 lig.CompCount = len(lig.Component) 

1200 ligs.append(lig) 

1201 

1202 

1203class COLR(BaseTable): 

1204 def decompile(self, reader, font): 

1205 # COLRv0 is exceptional in that LayerRecordCount appears *after* the 

1206 # LayerRecordArray it counts, but the parser logic expects Count fields 

1207 # to always precede the arrays. Here we work around this by parsing the 

1208 # LayerRecordCount before the rest of the table, and storing it in 

1209 # the reader's local state. 

1210 subReader = reader.getSubReader(offset=0) 

1211 for conv in self.getConverters(): 

1212 if conv.name != "LayerRecordCount": 

1213 subReader.advance(conv.staticSize) 

1214 continue 

1215 reader[conv.name] = conv.read(subReader, font, tableDict={}) 

1216 break 

1217 else: 

1218 raise AssertionError("LayerRecordCount converter not found") 

1219 return BaseTable.decompile(self, reader, font) 

1220 

1221 def preWrite(self, font): 

1222 # The writer similarly assumes Count values precede the things counted, 

1223 # thus here we pre-initialize a CountReference; the actual count value 

1224 # will be set to the lenght of the array by the time this is assembled. 

1225 self.LayerRecordCount = None 

1226 return { 

1227 **self.__dict__, 

1228 "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount"), 

1229 } 

1230 

1231 def computeClipBoxes(self, glyphSet: "_TTGlyphSet", quantization: int = 1): 

1232 if self.Version == 0: 

1233 return 

1234 

1235 clips = {} 

1236 for rec in self.BaseGlyphList.BaseGlyphPaintRecord: 

1237 try: 

1238 clipBox = rec.Paint.computeClipBox(self, glyphSet, quantization) 

1239 except Exception as e: 

1240 from fontTools.ttLib import TTLibError 

1241 

1242 raise TTLibError( 

1243 f"Failed to compute COLR ClipBox for {rec.BaseGlyph!r}" 

1244 ) from e 

1245 

1246 if clipBox is not None: 

1247 clips[rec.BaseGlyph] = clipBox 

1248 

1249 hasClipList = hasattr(self, "ClipList") and self.ClipList is not None 

1250 if not clips: 

1251 if hasClipList: 

1252 self.ClipList = None 

1253 else: 

1254 if not hasClipList: 

1255 self.ClipList = ClipList() 

1256 self.ClipList.Format = 1 

1257 self.ClipList.clips = clips 

1258 

1259 

1260class LookupList(BaseTable): 

1261 @property 

1262 def table(self): 

1263 for l in self.Lookup: 

1264 for st in l.SubTable: 

1265 if type(st).__name__.endswith("Subst"): 

1266 return "GSUB" 

1267 if type(st).__name__.endswith("Pos"): 

1268 return "GPOS" 

1269 raise ValueError 

1270 

1271 def toXML2(self, xmlWriter, font): 

1272 if ( 

1273 not font 

1274 or "Debg" not in font 

1275 or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data 

1276 ): 

1277 return super().toXML2(xmlWriter, font) 

1278 debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table] 

1279 for conv in self.getConverters(): 

1280 if conv.repeat: 

1281 value = getattr(self, conv.name, []) 

1282 for lookupIndex, item in enumerate(value): 

1283 if str(lookupIndex) in debugData: 

1284 info = LookupDebugInfo(*debugData[str(lookupIndex)]) 

1285 tag = info.location 

1286 if info.name: 

1287 tag = f"{info.name}: {tag}" 

1288 if info.feature: 

1289 script, language, feature = info.feature 

1290 tag = f"{tag} in {feature} ({script}/{language})" 

1291 xmlWriter.comment(tag) 

1292 xmlWriter.newline() 

1293 

1294 conv.xmlWrite( 

1295 xmlWriter, font, item, conv.name, [("index", lookupIndex)] 

1296 ) 

1297 else: 

1298 if conv.aux and not eval(conv.aux, None, vars(self)): 

1299 continue 

1300 value = getattr( 

1301 self, conv.name, None 

1302 ) # TODO Handle defaults instead of defaulting to None! 

1303 conv.xmlWrite(xmlWriter, font, value, conv.name, []) 

1304 

1305 

1306class BaseGlyphRecordArray(BaseTable): 

1307 def preWrite(self, font): 

1308 self.BaseGlyphRecord = sorted( 

1309 self.BaseGlyphRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph) 

1310 ) 

1311 return self.__dict__.copy() 

1312 

1313 

1314class BaseGlyphList(BaseTable): 

1315 def preWrite(self, font): 

1316 self.BaseGlyphPaintRecord = sorted( 

1317 self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph) 

1318 ) 

1319 return self.__dict__.copy() 

1320 

1321 

1322class ClipBoxFormat(IntEnum): 

1323 Static = 1 

1324 Variable = 2 

1325 

1326 def is_variable(self): 

1327 return self is self.Variable 

1328 

1329 def as_variable(self): 

1330 return self.Variable 

1331 

1332 

1333class ClipBox(getFormatSwitchingBaseTableClass("uint8")): 

1334 formatEnum = ClipBoxFormat 

1335 

1336 def as_tuple(self): 

1337 return tuple(getattr(self, conv.name) for conv in self.getConverters()) 

1338 

1339 def __repr__(self): 

1340 return f"{self.__class__.__name__}{self.as_tuple()}" 

1341 

1342 

1343class ClipList(getFormatSwitchingBaseTableClass("uint8")): 

1344 def populateDefaults(self, propagator=None): 

1345 if not hasattr(self, "clips"): 

1346 self.clips = {} 

1347 

1348 def postRead(self, rawTable, font): 

1349 clips = {} 

1350 glyphOrder = font.getGlyphOrder() 

1351 for i, rec in enumerate(rawTable["ClipRecord"]): 

1352 if rec.StartGlyphID > rec.EndGlyphID: 

1353 log.warning( 

1354 "invalid ClipRecord[%i].StartGlyphID (%i) > " 

1355 "EndGlyphID (%i); skipped", 

1356 i, 

1357 rec.StartGlyphID, 

1358 rec.EndGlyphID, 

1359 ) 

1360 continue 

1361 redefinedGlyphs = [] 

1362 missingGlyphs = [] 

1363 for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1): 

1364 try: 

1365 glyph = glyphOrder[glyphID] 

1366 except IndexError: 

1367 missingGlyphs.append(glyphID) 

1368 continue 

1369 if glyph not in clips: 

1370 clips[glyph] = copy.copy(rec.ClipBox) 

1371 else: 

1372 redefinedGlyphs.append(glyphID) 

1373 if redefinedGlyphs: 

1374 log.warning( 

1375 "ClipRecord[%i] overlaps previous records; " 

1376 "ignoring redefined clip boxes for the " 

1377 "following glyph ID range: [%i-%i]", 

1378 i, 

1379 min(redefinedGlyphs), 

1380 max(redefinedGlyphs), 

1381 ) 

1382 if missingGlyphs: 

1383 log.warning( 

1384 "ClipRecord[%i] range references missing " "glyph IDs: [%i-%i]", 

1385 i, 

1386 min(missingGlyphs), 

1387 max(missingGlyphs), 

1388 ) 

1389 self.clips = clips 

1390 

1391 def groups(self): 

1392 glyphsByClip = defaultdict(list) 

1393 uniqueClips = {} 

1394 for glyphName, clipBox in self.clips.items(): 

1395 key = clipBox.as_tuple() 

1396 glyphsByClip[key].append(glyphName) 

1397 if key not in uniqueClips: 

1398 uniqueClips[key] = clipBox 

1399 return { 

1400 frozenset(glyphs): uniqueClips[key] for key, glyphs in glyphsByClip.items() 

1401 } 

1402 

1403 def preWrite(self, font): 

1404 if not hasattr(self, "clips"): 

1405 self.clips = {} 

1406 clipBoxRanges = {} 

1407 glyphMap = font.getReverseGlyphMap() 

1408 for glyphs, clipBox in self.groups().items(): 

1409 glyphIDs = sorted( 

1410 glyphMap[glyphName] for glyphName in glyphs if glyphName in glyphMap 

1411 ) 

1412 if not glyphIDs: 

1413 continue 

1414 last = glyphIDs[0] 

1415 ranges = [[last]] 

1416 for glyphID in glyphIDs[1:]: 

1417 if glyphID != last + 1: 

1418 ranges[-1].append(last) 

1419 ranges.append([glyphID]) 

1420 last = glyphID 

1421 ranges[-1].append(last) 

1422 for start, end in ranges: 

1423 assert (start, end) not in clipBoxRanges 

1424 clipBoxRanges[(start, end)] = clipBox 

1425 

1426 clipRecords = [] 

1427 for (start, end), clipBox in sorted(clipBoxRanges.items()): 

1428 record = ClipRecord() 

1429 record.StartGlyphID = start 

1430 record.EndGlyphID = end 

1431 record.ClipBox = clipBox 

1432 clipRecords.append(record) 

1433 rawTable = { 

1434 "ClipCount": len(clipRecords), 

1435 "ClipRecord": clipRecords, 

1436 } 

1437 return rawTable 

1438 

1439 def toXML(self, xmlWriter, font, attrs=None, name=None): 

1440 tableName = name if name else self.__class__.__name__ 

1441 if attrs is None: 

1442 attrs = [] 

1443 if hasattr(self, "Format"): 

1444 attrs.append(("Format", self.Format)) 

1445 xmlWriter.begintag(tableName, attrs) 

1446 xmlWriter.newline() 

1447 # sort clips alphabetically to ensure deterministic XML dump 

1448 for glyphs, clipBox in sorted( 

1449 self.groups().items(), key=lambda item: min(item[0]) 

1450 ): 

1451 xmlWriter.begintag("Clip") 

1452 xmlWriter.newline() 

1453 for glyphName in sorted(glyphs): 

1454 xmlWriter.simpletag("Glyph", value=glyphName) 

1455 xmlWriter.newline() 

1456 xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)]) 

1457 xmlWriter.newline() 

1458 clipBox.toXML2(xmlWriter, font) 

1459 xmlWriter.endtag("ClipBox") 

1460 xmlWriter.newline() 

1461 xmlWriter.endtag("Clip") 

1462 xmlWriter.newline() 

1463 xmlWriter.endtag(tableName) 

1464 xmlWriter.newline() 

1465 

1466 def fromXML(self, name, attrs, content, font): 

1467 clips = getattr(self, "clips", None) 

1468 if clips is None: 

1469 self.clips = clips = {} 

1470 assert name == "Clip" 

1471 glyphs = [] 

1472 clipBox = None 

1473 for elem in content: 

1474 if not isinstance(elem, tuple): 

1475 continue 

1476 name, attrs, content = elem 

1477 if name == "Glyph": 

1478 glyphs.append(attrs["value"]) 

1479 elif name == "ClipBox": 

1480 clipBox = ClipBox() 

1481 clipBox.Format = safeEval(attrs["Format"]) 

1482 for elem in content: 

1483 if not isinstance(elem, tuple): 

1484 continue 

1485 name, attrs, content = elem 

1486 clipBox.fromXML(name, attrs, content, font) 

1487 if clipBox: 

1488 for glyphName in glyphs: 

1489 clips[glyphName] = clipBox 

1490 

1491 

1492class ExtendMode(IntEnum): 

1493 PAD = 0 

1494 REPEAT = 1 

1495 REFLECT = 2 

1496 

1497 

1498# Porter-Duff modes for COLRv1 PaintComposite: 

1499# https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration 

1500class CompositeMode(IntEnum): 

1501 CLEAR = 0 

1502 SRC = 1 

1503 DEST = 2 

1504 SRC_OVER = 3 

1505 DEST_OVER = 4 

1506 SRC_IN = 5 

1507 DEST_IN = 6 

1508 SRC_OUT = 7 

1509 DEST_OUT = 8 

1510 SRC_ATOP = 9 

1511 DEST_ATOP = 10 

1512 XOR = 11 

1513 PLUS = 12 

1514 SCREEN = 13 

1515 OVERLAY = 14 

1516 DARKEN = 15 

1517 LIGHTEN = 16 

1518 COLOR_DODGE = 17 

1519 COLOR_BURN = 18 

1520 HARD_LIGHT = 19 

1521 SOFT_LIGHT = 20 

1522 DIFFERENCE = 21 

1523 EXCLUSION = 22 

1524 MULTIPLY = 23 

1525 HSL_HUE = 24 

1526 HSL_SATURATION = 25 

1527 HSL_COLOR = 26 

1528 HSL_LUMINOSITY = 27 

1529 

1530 

1531class PaintFormat(IntEnum): 

1532 PaintColrLayers = 1 

1533 PaintSolid = 2 

1534 PaintVarSolid = 3 

1535 PaintLinearGradient = 4 

1536 PaintVarLinearGradient = 5 

1537 PaintRadialGradient = 6 

1538 PaintVarRadialGradient = 7 

1539 PaintSweepGradient = 8 

1540 PaintVarSweepGradient = 9 

1541 PaintGlyph = 10 

1542 PaintColrGlyph = 11 

1543 PaintTransform = 12 

1544 PaintVarTransform = 13 

1545 PaintTranslate = 14 

1546 PaintVarTranslate = 15 

1547 PaintScale = 16 

1548 PaintVarScale = 17 

1549 PaintScaleAroundCenter = 18 

1550 PaintVarScaleAroundCenter = 19 

1551 PaintScaleUniform = 20 

1552 PaintVarScaleUniform = 21 

1553 PaintScaleUniformAroundCenter = 22 

1554 PaintVarScaleUniformAroundCenter = 23 

1555 PaintRotate = 24 

1556 PaintVarRotate = 25 

1557 PaintRotateAroundCenter = 26 

1558 PaintVarRotateAroundCenter = 27 

1559 PaintSkew = 28 

1560 PaintVarSkew = 29 

1561 PaintSkewAroundCenter = 30 

1562 PaintVarSkewAroundCenter = 31 

1563 PaintComposite = 32 

1564 

1565 def is_variable(self): 

1566 return self.name.startswith("PaintVar") 

1567 

1568 def as_variable(self): 

1569 if self.is_variable(): 

1570 return self 

1571 try: 

1572 return PaintFormat.__members__[f"PaintVar{self.name[5:]}"] 

1573 except KeyError: 

1574 return None 

1575 

1576 

1577class Paint(getFormatSwitchingBaseTableClass("uint8")): 

1578 formatEnum = PaintFormat 

1579 

1580 def getFormatName(self): 

1581 try: 

1582 return self.formatEnum(self.Format).name 

1583 except ValueError: 

1584 raise NotImplementedError(f"Unknown Paint format: {self.Format}") 

1585 

1586 def toXML(self, xmlWriter, font, attrs=None, name=None): 

1587 tableName = name if name else self.__class__.__name__ 

1588 if attrs is None: 

1589 attrs = [] 

1590 attrs.append(("Format", self.Format)) 

1591 xmlWriter.begintag(tableName, attrs) 

1592 xmlWriter.comment(self.getFormatName()) 

1593 xmlWriter.newline() 

1594 self.toXML2(xmlWriter, font) 

1595 xmlWriter.endtag(tableName) 

1596 xmlWriter.newline() 

1597 

1598 def iterPaintSubTables(self, colr: COLR) -> Iterator[BaseTable.SubTableEntry]: 

1599 if self.Format == PaintFormat.PaintColrLayers: 

1600 # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists 

1601 layers = [] 

1602 if colr.LayerList is not None: 

1603 layers = colr.LayerList.Paint 

1604 yield from ( 

1605 BaseTable.SubTableEntry(name="Layers", value=v, index=i) 

1606 for i, v in enumerate( 

1607 layers[self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers] 

1608 ) 

1609 ) 

1610 return 

1611 

1612 if self.Format == PaintFormat.PaintColrGlyph: 

1613 for record in colr.BaseGlyphList.BaseGlyphPaintRecord: 

1614 if record.BaseGlyph == self.Glyph: 

1615 yield BaseTable.SubTableEntry(name="BaseGlyph", value=record.Paint) 

1616 return 

1617 else: 

1618 raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList") 

1619 

1620 for conv in self.getConverters(): 

1621 if conv.tableClass is not None and issubclass(conv.tableClass, type(self)): 

1622 value = getattr(self, conv.name) 

1623 yield BaseTable.SubTableEntry(name=conv.name, value=value) 

1624 

1625 def getChildren(self, colr) -> List["Paint"]: 

1626 # this is kept for backward compatibility (e.g. it's used by the subsetter) 

1627 return [p.value for p in self.iterPaintSubTables(colr)] 

1628 

1629 def traverse(self, colr: COLR, callback): 

1630 """Depth-first traversal of graph rooted at self, callback on each node.""" 

1631 if not callable(callback): 

1632 raise TypeError("callback must be callable") 

1633 

1634 for path in dfs_base_table( 

1635 self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr) 

1636 ): 

1637 paint = path[-1].value 

1638 callback(paint) 

1639 

1640 def getTransform(self) -> Transform: 

1641 if self.Format == PaintFormat.PaintTransform: 

1642 t = self.Transform 

1643 return Transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy) 

1644 elif self.Format == PaintFormat.PaintTranslate: 

1645 return Identity.translate(self.dx, self.dy) 

1646 elif self.Format == PaintFormat.PaintScale: 

1647 return Identity.scale(self.scaleX, self.scaleY) 

1648 elif self.Format == PaintFormat.PaintScaleAroundCenter: 

1649 return ( 

1650 Identity.translate(self.centerX, self.centerY) 

1651 .scale(self.scaleX, self.scaleY) 

1652 .translate(-self.centerX, -self.centerY) 

1653 ) 

1654 elif self.Format == PaintFormat.PaintScaleUniform: 

1655 return Identity.scale(self.scale) 

1656 elif self.Format == PaintFormat.PaintScaleUniformAroundCenter: 

1657 return ( 

1658 Identity.translate(self.centerX, self.centerY) 

1659 .scale(self.scale) 

1660 .translate(-self.centerX, -self.centerY) 

1661 ) 

1662 elif self.Format == PaintFormat.PaintRotate: 

1663 return Identity.rotate(radians(self.angle)) 

1664 elif self.Format == PaintFormat.PaintRotateAroundCenter: 

1665 return ( 

1666 Identity.translate(self.centerX, self.centerY) 

1667 .rotate(radians(self.angle)) 

1668 .translate(-self.centerX, -self.centerY) 

1669 ) 

1670 elif self.Format == PaintFormat.PaintSkew: 

1671 return Identity.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle)) 

1672 elif self.Format == PaintFormat.PaintSkewAroundCenter: 

1673 return ( 

1674 Identity.translate(self.centerX, self.centerY) 

1675 .skew(radians(-self.xSkewAngle), radians(self.ySkewAngle)) 

1676 .translate(-self.centerX, -self.centerY) 

1677 ) 

1678 if PaintFormat(self.Format).is_variable(): 

1679 raise NotImplementedError(f"Variable Paints not supported: {self.Format}") 

1680 

1681 return Identity 

1682 

1683 def computeClipBox( 

1684 self, colr: COLR, glyphSet: "_TTGlyphSet", quantization: int = 1 

1685 ) -> Optional[ClipBox]: 

1686 pen = ControlBoundsPen(glyphSet) 

1687 for path in dfs_base_table( 

1688 self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr) 

1689 ): 

1690 paint = path[-1].value 

1691 if paint.Format == PaintFormat.PaintGlyph: 

1692 transformation = reduce( 

1693 Transform.transform, 

1694 (st.value.getTransform() for st in path), 

1695 Identity, 

1696 ) 

1697 glyphSet[paint.Glyph].draw(TransformPen(pen, transformation)) 

1698 

1699 if pen.bounds is None: 

1700 return None 

1701 

1702 cb = ClipBox() 

1703 cb.Format = int(ClipBoxFormat.Static) 

1704 cb.xMin, cb.yMin, cb.xMax, cb.yMax = quantizeRect(pen.bounds, quantization) 

1705 return cb 

1706 

1707 

1708# For each subtable format there is a class. However, we don't really distinguish 

1709# between "field name" and "format name": often these are the same. Yet there's 

1710# a whole bunch of fields with different names. The following dict is a mapping 

1711# from "format name" to "field name". _buildClasses() uses this to create a 

1712# subclass for each alternate field name. 

1713# 

1714_equivalents = { 

1715 "MarkArray": ("Mark1Array",), 

1716 "LangSys": ("DefaultLangSys",), 

1717 "Coverage": ( 

1718 "MarkCoverage", 

1719 "BaseCoverage", 

1720 "LigatureCoverage", 

1721 "Mark1Coverage", 

1722 "Mark2Coverage", 

1723 "BacktrackCoverage", 

1724 "InputCoverage", 

1725 "LookAheadCoverage", 

1726 "VertGlyphCoverage", 

1727 "HorizGlyphCoverage", 

1728 "TopAccentCoverage", 

1729 "ExtendedShapeCoverage", 

1730 "MathKernCoverage", 

1731 ), 

1732 "ClassDef": ( 

1733 "ClassDef1", 

1734 "ClassDef2", 

1735 "BacktrackClassDef", 

1736 "InputClassDef", 

1737 "LookAheadClassDef", 

1738 "GlyphClassDef", 

1739 "MarkAttachClassDef", 

1740 ), 

1741 "Anchor": ( 

1742 "EntryAnchor", 

1743 "ExitAnchor", 

1744 "BaseAnchor", 

1745 "LigatureAnchor", 

1746 "Mark2Anchor", 

1747 "MarkAnchor", 

1748 ), 

1749 "Device": ( 

1750 "XPlaDevice", 

1751 "YPlaDevice", 

1752 "XAdvDevice", 

1753 "YAdvDevice", 

1754 "XDeviceTable", 

1755 "YDeviceTable", 

1756 "DeviceTable", 

1757 ), 

1758 "Axis": ( 

1759 "HorizAxis", 

1760 "VertAxis", 

1761 ), 

1762 "MinMax": ("DefaultMinMax",), 

1763 "BaseCoord": ( 

1764 "MinCoord", 

1765 "MaxCoord", 

1766 ), 

1767 "JstfLangSys": ("DefJstfLangSys",), 

1768 "JstfGSUBModList": ( 

1769 "ShrinkageEnableGSUB", 

1770 "ShrinkageDisableGSUB", 

1771 "ExtensionEnableGSUB", 

1772 "ExtensionDisableGSUB", 

1773 ), 

1774 "JstfGPOSModList": ( 

1775 "ShrinkageEnableGPOS", 

1776 "ShrinkageDisableGPOS", 

1777 "ExtensionEnableGPOS", 

1778 "ExtensionDisableGPOS", 

1779 ), 

1780 "JstfMax": ( 

1781 "ShrinkageJstfMax", 

1782 "ExtensionJstfMax", 

1783 ), 

1784 "MathKern": ( 

1785 "TopRightMathKern", 

1786 "TopLeftMathKern", 

1787 "BottomRightMathKern", 

1788 "BottomLeftMathKern", 

1789 ), 

1790 "MathGlyphConstruction": ("VertGlyphConstruction", "HorizGlyphConstruction"), 

1791} 

1792 

1793# 

1794# OverFlow logic, to automatically create ExtensionLookups 

1795# XXX This should probably move to otBase.py 

1796# 

1797 

1798 

1799def fixLookupOverFlows(ttf, overflowRecord): 

1800 """Either the offset from the LookupList to a lookup overflowed, or 

1801 an offset from a lookup to a subtable overflowed. 

1802 The table layout is: 

1803 GPSO/GUSB 

1804 Script List 

1805 Feature List 

1806 LookUpList 

1807 Lookup[0] and contents 

1808 SubTable offset list 

1809 SubTable[0] and contents 

1810 ... 

1811 SubTable[n] and contents 

1812 ... 

1813 Lookup[n] and contents 

1814 SubTable offset list 

1815 SubTable[0] and contents 

1816 ... 

1817 SubTable[n] and contents 

1818 If the offset to a lookup overflowed (SubTableIndex is None) 

1819 we must promote the *previous* lookup to an Extension type. 

1820 If the offset from a lookup to subtable overflowed, then we must promote it 

1821 to an Extension Lookup type. 

1822 """ 

1823 ok = 0 

1824 lookupIndex = overflowRecord.LookupListIndex 

1825 if overflowRecord.SubTableIndex is None: 

1826 lookupIndex = lookupIndex - 1 

1827 if lookupIndex < 0: 

1828 return ok 

1829 if overflowRecord.tableType == "GSUB": 

1830 extType = 7 

1831 elif overflowRecord.tableType == "GPOS": 

1832 extType = 9 

1833 

1834 lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup 

1835 lookup = lookups[lookupIndex] 

1836 # If the previous lookup is an extType, look further back. Very unlikely, but possible. 

1837 while lookup.SubTable[0].__class__.LookupType == extType: 

1838 lookupIndex = lookupIndex - 1 

1839 if lookupIndex < 0: 

1840 return ok 

1841 lookup = lookups[lookupIndex] 

1842 

1843 for lookupIndex in range(lookupIndex, len(lookups)): 

1844 lookup = lookups[lookupIndex] 

1845 if lookup.LookupType != extType: 

1846 lookup.LookupType = extType 

1847 for si in range(len(lookup.SubTable)): 

1848 subTable = lookup.SubTable[si] 

1849 extSubTableClass = lookupTypes[overflowRecord.tableType][extType] 

1850 extSubTable = extSubTableClass() 

1851 extSubTable.Format = 1 

1852 extSubTable.ExtSubTable = subTable 

1853 lookup.SubTable[si] = extSubTable 

1854 ok = 1 

1855 return ok 

1856 

1857 

1858def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord): 

1859 ok = 1 

1860 oldMapping = sorted(oldSubTable.mapping.items()) 

1861 oldLen = len(oldMapping) 

1862 

1863 if overflowRecord.itemName in ["Coverage", "RangeRecord"]: 

1864 # Coverage table is written last. Overflow is to or within the 

1865 # the coverage table. We will just cut the subtable in half. 

1866 newLen = oldLen // 2 

1867 

1868 elif overflowRecord.itemName == "Sequence": 

1869 # We just need to back up by two items from the overflowed 

1870 # Sequence index to make sure the offset to the Coverage table 

1871 # doesn't overflow. 

1872 newLen = overflowRecord.itemIndex - 1 

1873 

1874 newSubTable.mapping = {} 

1875 for i in range(newLen, oldLen): 

1876 item = oldMapping[i] 

1877 key = item[0] 

1878 newSubTable.mapping[key] = item[1] 

1879 del oldSubTable.mapping[key] 

1880 

1881 return ok 

1882 

1883 

1884def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): 

1885 ok = 1 

1886 if hasattr(oldSubTable, "sortCoverageLast"): 

1887 newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast 

1888 

1889 oldAlts = sorted(oldSubTable.alternates.items()) 

1890 oldLen = len(oldAlts) 

1891 

1892 if overflowRecord.itemName in ["Coverage", "RangeRecord"]: 

1893 # Coverage table is written last. overflow is to or within the 

1894 # the coverage table. We will just cut the subtable in half. 

1895 newLen = oldLen // 2 

1896 

1897 elif overflowRecord.itemName == "AlternateSet": 

1898 # We just need to back up by two items 

1899 # from the overflowed AlternateSet index to make sure the offset 

1900 # to the Coverage table doesn't overflow. 

1901 newLen = overflowRecord.itemIndex - 1 

1902 

1903 newSubTable.alternates = {} 

1904 for i in range(newLen, oldLen): 

1905 item = oldAlts[i] 

1906 key = item[0] 

1907 newSubTable.alternates[key] = item[1] 

1908 del oldSubTable.alternates[key] 

1909 

1910 return ok 

1911 

1912 

1913def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord): 

1914 ok = 1 

1915 oldLigs = sorted(oldSubTable.ligatures.items()) 

1916 oldLen = len(oldLigs) 

1917 

1918 if overflowRecord.itemName in ["Coverage", "RangeRecord"]: 

1919 # Coverage table is written last. overflow is to or within the 

1920 # the coverage table. We will just cut the subtable in half. 

1921 newLen = oldLen // 2 

1922 

1923 elif overflowRecord.itemName == "LigatureSet": 

1924 # We just need to back up by two items 

1925 # from the overflowed AlternateSet index to make sure the offset 

1926 # to the Coverage table doesn't overflow. 

1927 newLen = overflowRecord.itemIndex - 1 

1928 

1929 newSubTable.ligatures = {} 

1930 for i in range(newLen, oldLen): 

1931 item = oldLigs[i] 

1932 key = item[0] 

1933 newSubTable.ligatures[key] = item[1] 

1934 del oldSubTable.ligatures[key] 

1935 

1936 return ok 

1937 

1938 

1939def splitPairPos(oldSubTable, newSubTable, overflowRecord): 

1940 st = oldSubTable 

1941 ok = False 

1942 newSubTable.Format = oldSubTable.Format 

1943 if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1: 

1944 for name in "ValueFormat1", "ValueFormat2": 

1945 setattr(newSubTable, name, getattr(oldSubTable, name)) 

1946 

1947 # Move top half of coverage to new subtable 

1948 

1949 newSubTable.Coverage = oldSubTable.Coverage.__class__() 

1950 

1951 coverage = oldSubTable.Coverage.glyphs 

1952 records = oldSubTable.PairSet 

1953 

1954 oldCount = len(oldSubTable.PairSet) // 2 

1955 

1956 oldSubTable.Coverage.glyphs = coverage[:oldCount] 

1957 oldSubTable.PairSet = records[:oldCount] 

1958 

1959 newSubTable.Coverage.glyphs = coverage[oldCount:] 

1960 newSubTable.PairSet = records[oldCount:] 

1961 

1962 oldSubTable.PairSetCount = len(oldSubTable.PairSet) 

1963 newSubTable.PairSetCount = len(newSubTable.PairSet) 

1964 

1965 ok = True 

1966 

1967 elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1: 

1968 if not hasattr(oldSubTable, "Class2Count"): 

1969 oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record) 

1970 for name in "Class2Count", "ClassDef2", "ValueFormat1", "ValueFormat2": 

1971 setattr(newSubTable, name, getattr(oldSubTable, name)) 

1972 

1973 # The two subtables will still have the same ClassDef2 and the table 

1974 # sharing will still cause the sharing to overflow. As such, disable 

1975 # sharing on the one that is serialized second (that's oldSubTable). 

1976 oldSubTable.DontShare = True 

1977 

1978 # Move top half of class numbers to new subtable 

1979 

1980 newSubTable.Coverage = oldSubTable.Coverage.__class__() 

1981 newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__() 

1982 

1983 coverage = oldSubTable.Coverage.glyphs 

1984 classDefs = oldSubTable.ClassDef1.classDefs 

1985 records = oldSubTable.Class1Record 

1986 

1987 oldCount = len(oldSubTable.Class1Record) // 2 

1988 newGlyphs = set(k for k, v in classDefs.items() if v >= oldCount) 

1989 

1990 oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs] 

1991 oldSubTable.ClassDef1.classDefs = { 

1992 k: v for k, v in classDefs.items() if v < oldCount 

1993 } 

1994 oldSubTable.Class1Record = records[:oldCount] 

1995 

1996 newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs] 

1997 newSubTable.ClassDef1.classDefs = { 

1998 k: (v - oldCount) for k, v in classDefs.items() if v > oldCount 

1999 } 

2000 newSubTable.Class1Record = records[oldCount:] 

2001 

2002 oldSubTable.Class1Count = len(oldSubTable.Class1Record) 

2003 newSubTable.Class1Count = len(newSubTable.Class1Record) 

2004 

2005 ok = True 

2006 

2007 return ok 

2008 

2009 

2010def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord): 

2011 # split half of the mark classes to the new subtable 

2012 classCount = oldSubTable.ClassCount 

2013 if classCount < 2: 

2014 # oh well, not much left to split... 

2015 return False 

2016 

2017 oldClassCount = classCount // 2 

2018 newClassCount = classCount - oldClassCount 

2019 

2020 oldMarkCoverage, oldMarkRecords = [], [] 

2021 newMarkCoverage, newMarkRecords = [], [] 

2022 for glyphName, markRecord in zip( 

2023 oldSubTable.MarkCoverage.glyphs, oldSubTable.MarkArray.MarkRecord 

2024 ): 

2025 if markRecord.Class < oldClassCount: 

2026 oldMarkCoverage.append(glyphName) 

2027 oldMarkRecords.append(markRecord) 

2028 else: 

2029 markRecord.Class -= oldClassCount 

2030 newMarkCoverage.append(glyphName) 

2031 newMarkRecords.append(markRecord) 

2032 

2033 oldBaseRecords, newBaseRecords = [], [] 

2034 for rec in oldSubTable.BaseArray.BaseRecord: 

2035 oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__() 

2036 oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount] 

2037 newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:] 

2038 oldBaseRecords.append(oldBaseRecord) 

2039 newBaseRecords.append(newBaseRecord) 

2040 

2041 newSubTable.Format = oldSubTable.Format 

2042 

2043 oldSubTable.MarkCoverage.glyphs = oldMarkCoverage 

2044 newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__() 

2045 newSubTable.MarkCoverage.glyphs = newMarkCoverage 

2046 

2047 # share the same BaseCoverage in both halves 

2048 newSubTable.BaseCoverage = oldSubTable.BaseCoverage 

2049 

2050 oldSubTable.ClassCount = oldClassCount 

2051 newSubTable.ClassCount = newClassCount 

2052 

2053 oldSubTable.MarkArray.MarkRecord = oldMarkRecords 

2054 newSubTable.MarkArray = oldSubTable.MarkArray.__class__() 

2055 newSubTable.MarkArray.MarkRecord = newMarkRecords 

2056 

2057 oldSubTable.MarkArray.MarkCount = len(oldMarkRecords) 

2058 newSubTable.MarkArray.MarkCount = len(newMarkRecords) 

2059 

2060 oldSubTable.BaseArray.BaseRecord = oldBaseRecords 

2061 newSubTable.BaseArray = oldSubTable.BaseArray.__class__() 

2062 newSubTable.BaseArray.BaseRecord = newBaseRecords 

2063 

2064 oldSubTable.BaseArray.BaseCount = len(oldBaseRecords) 

2065 newSubTable.BaseArray.BaseCount = len(newBaseRecords) 

2066 

2067 return True 

2068 

2069 

2070splitTable = { 

2071 "GSUB": { 

2072 # 1: splitSingleSubst, 

2073 2: splitMultipleSubst, 

2074 3: splitAlternateSubst, 

2075 4: splitLigatureSubst, 

2076 # 5: splitContextSubst, 

2077 # 6: splitChainContextSubst, 

2078 # 7: splitExtensionSubst, 

2079 # 8: splitReverseChainSingleSubst, 

2080 }, 

2081 "GPOS": { 

2082 # 1: splitSinglePos, 

2083 2: splitPairPos, 

2084 # 3: splitCursivePos, 

2085 4: splitMarkBasePos, 

2086 # 5: splitMarkLigPos, 

2087 # 6: splitMarkMarkPos, 

2088 # 7: splitContextPos, 

2089 # 8: splitChainContextPos, 

2090 # 9: splitExtensionPos, 

2091 }, 

2092} 

2093 

2094 

2095def fixSubTableOverFlows(ttf, overflowRecord): 

2096 """ 

2097 An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts. 

2098 """ 

2099 table = ttf[overflowRecord.tableType].table 

2100 lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex] 

2101 subIndex = overflowRecord.SubTableIndex 

2102 subtable = lookup.SubTable[subIndex] 

2103 

2104 # First, try not sharing anything for this subtable... 

2105 if not hasattr(subtable, "DontShare"): 

2106 subtable.DontShare = True 

2107 return True 

2108 

2109 if hasattr(subtable, "ExtSubTable"): 

2110 # We split the subtable of the Extension table, and add a new Extension table 

2111 # to contain the new subtable. 

2112 

2113 subTableType = subtable.ExtSubTable.__class__.LookupType 

2114 extSubTable = subtable 

2115 subtable = extSubTable.ExtSubTable 

2116 newExtSubTableClass = lookupTypes[overflowRecord.tableType][ 

2117 extSubTable.__class__.LookupType 

2118 ] 

2119 newExtSubTable = newExtSubTableClass() 

2120 newExtSubTable.Format = extSubTable.Format 

2121 toInsert = newExtSubTable 

2122 

2123 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] 

2124 newSubTable = newSubTableClass() 

2125 newExtSubTable.ExtSubTable = newSubTable 

2126 else: 

2127 subTableType = subtable.__class__.LookupType 

2128 newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] 

2129 newSubTable = newSubTableClass() 

2130 toInsert = newSubTable 

2131 

2132 if hasattr(lookup, "SubTableCount"): # may not be defined yet. 

2133 lookup.SubTableCount = lookup.SubTableCount + 1 

2134 

2135 try: 

2136 splitFunc = splitTable[overflowRecord.tableType][subTableType] 

2137 except KeyError: 

2138 log.error( 

2139 "Don't know how to split %s lookup type %s", 

2140 overflowRecord.tableType, 

2141 subTableType, 

2142 ) 

2143 return False 

2144 

2145 ok = splitFunc(subtable, newSubTable, overflowRecord) 

2146 if ok: 

2147 lookup.SubTable.insert(subIndex + 1, toInsert) 

2148 return ok 

2149 

2150 

2151# End of OverFlow logic 

2152 

2153 

2154def _buildClasses(): 

2155 import re 

2156 from .otData import otData 

2157 

2158 formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$") 

2159 namespace = globals() 

2160 

2161 # populate module with classes 

2162 for name, table in otData: 

2163 baseClass = BaseTable 

2164 m = formatPat.match(name) 

2165 if m: 

2166 # XxxFormatN subtable, we only add the "base" table 

2167 name = m.group(1) 

2168 # the first row of a format-switching otData table describes the Format; 

2169 # the first column defines the type of the Format field. 

2170 # Currently this can be either 'uint16' or 'uint8'. 

2171 formatType = table[0][0] 

2172 baseClass = getFormatSwitchingBaseTableClass(formatType) 

2173 if name not in namespace: 

2174 # the class doesn't exist yet, so the base implementation is used. 

2175 cls = type(name, (baseClass,), {}) 

2176 if name in ("GSUB", "GPOS"): 

2177 cls.DontShare = True 

2178 namespace[name] = cls 

2179 

2180 # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.) 

2181 for name, _ in otData: 

2182 if name.startswith("Var") and len(name) > 3 and name[3:] in namespace: 

2183 varType = namespace[name] 

2184 noVarType = namespace[name[3:]] 

2185 varType.NoVarType = noVarType 

2186 noVarType.VarType = varType 

2187 

2188 for base, alts in _equivalents.items(): 

2189 base = namespace[base] 

2190 for alt in alts: 

2191 namespace[alt] = base 

2192 

2193 global lookupTypes 

2194 lookupTypes = { 

2195 "GSUB": { 

2196 1: SingleSubst, 

2197 2: MultipleSubst, 

2198 3: AlternateSubst, 

2199 4: LigatureSubst, 

2200 5: ContextSubst, 

2201 6: ChainContextSubst, 

2202 7: ExtensionSubst, 

2203 8: ReverseChainSingleSubst, 

2204 }, 

2205 "GPOS": { 

2206 1: SinglePos, 

2207 2: PairPos, 

2208 3: CursivePos, 

2209 4: MarkBasePos, 

2210 5: MarkLigPos, 

2211 6: MarkMarkPos, 

2212 7: ContextPos, 

2213 8: ChainContextPos, 

2214 9: ExtensionPos, 

2215 }, 

2216 "mort": { 

2217 4: NoncontextualMorph, 

2218 }, 

2219 "morx": { 

2220 0: RearrangementMorph, 

2221 1: ContextualMorph, 

2222 2: LigatureMorph, 

2223 # 3: Reserved, 

2224 4: NoncontextualMorph, 

2225 5: InsertionMorph, 

2226 }, 

2227 } 

2228 lookupTypes["JSTF"] = lookupTypes["GPOS"] # JSTF contains GPOS 

2229 for lookupEnum in lookupTypes.values(): 

2230 for enum, cls in lookupEnum.items(): 

2231 cls.LookupType = enum 

2232 

2233 global featureParamTypes 

2234 featureParamTypes = { 

2235 "size": FeatureParamsSize, 

2236 } 

2237 for i in range(1, 20 + 1): 

2238 featureParamTypes["ss%02d" % i] = FeatureParamsStylisticSet 

2239 for i in range(1, 99 + 1): 

2240 featureParamTypes["cv%02d" % i] = FeatureParamsCharacterVariants 

2241 

2242 # add converters to classes 

2243 from .otConverters import buildConverters 

2244 

2245 for name, table in otData: 

2246 m = formatPat.match(name) 

2247 if m: 

2248 # XxxFormatN subtable, add converter to "base" table 

2249 name, format = m.groups() 

2250 format = int(format) 

2251 cls = namespace[name] 

2252 if not hasattr(cls, "converters"): 

2253 cls.converters = {} 

2254 cls.convertersByName = {} 

2255 converters, convertersByName = buildConverters(table[1:], namespace) 

2256 cls.converters[format] = converters 

2257 cls.convertersByName[format] = convertersByName 

2258 # XXX Add staticSize? 

2259 else: 

2260 cls = namespace[name] 

2261 cls.converters, cls.convertersByName = buildConverters(table, namespace) 

2262 # XXX Add staticSize? 

2263 

2264 

2265_buildClasses() 

2266 

2267 

2268def _getGlyphsFromCoverageTable(coverage): 

2269 if coverage is None: 

2270 # empty coverage table 

2271 return [] 

2272 else: 

2273 return coverage.glyphs