Coverage for /usr/lib/python3/dist-packages/fontTools/varLib/merger.py: 14%

1009 statements  

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

1""" 

2Merge OpenType Layout tables (GDEF / GPOS / GSUB). 

3""" 

4import os 

5import copy 

6import enum 

7from operator import ior 

8import logging 

9from fontTools.colorLib.builder import MAX_PAINT_COLR_LAYER_COUNT, LayerReuseCache 

10from fontTools.misc import classifyTools 

11from fontTools.misc.roundTools import otRound 

12from fontTools.misc.treeTools import build_n_ary_tree 

13from fontTools.ttLib.tables import otTables as ot 

14from fontTools.ttLib.tables import otBase as otBase 

15from fontTools.ttLib.tables.otConverters import BaseFixedValue 

16from fontTools.ttLib.tables.otTraverse import dfs_base_table 

17from fontTools.ttLib.tables.DefaultTable import DefaultTable 

18from fontTools.varLib import builder, models, varStore 

19from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo, subList 

20from fontTools.varLib.varStore import VarStoreInstancer 

21from functools import reduce 

22from fontTools.otlLib.builder import buildSinglePos 

23from fontTools.otlLib.optimize.gpos import ( 

24 _compression_level_from_env, 

25 compact_pair_pos, 

26) 

27 

28log = logging.getLogger("fontTools.varLib.merger") 

29 

30from .errors import ( 

31 ShouldBeConstant, 

32 FoundANone, 

33 MismatchedTypes, 

34 NotANone, 

35 LengthsDiffer, 

36 KeysDiffer, 

37 InconsistentGlyphOrder, 

38 InconsistentExtensions, 

39 InconsistentFormats, 

40 UnsupportedFormat, 

41 VarLibMergeError, 

42) 

43 

44 

45class Merger(object): 

46 def __init__(self, font=None): 

47 self.font = font 

48 # mergeTables populates this from the parent's master ttfs 

49 self.ttfs = None 

50 

51 @classmethod 

52 def merger(celf, clazzes, attrs=(None,)): 

53 assert celf != Merger, "Subclass Merger instead." 

54 if "mergers" not in celf.__dict__: 

55 celf.mergers = {} 

56 if type(clazzes) in (type, enum.EnumMeta): 

57 clazzes = (clazzes,) 

58 if type(attrs) == str: 

59 attrs = (attrs,) 

60 

61 def wrapper(method): 

62 assert method.__name__ == "merge" 

63 done = [] 

64 for clazz in clazzes: 

65 if clazz in done: 

66 continue # Support multiple names of a clazz 

67 done.append(clazz) 

68 mergers = celf.mergers.setdefault(clazz, {}) 

69 for attr in attrs: 

70 assert attr not in mergers, ( 

71 "Oops, class '%s' has merge function for '%s' defined already." 

72 % (clazz.__name__, attr) 

73 ) 

74 mergers[attr] = method 

75 return None 

76 

77 return wrapper 

78 

79 @classmethod 

80 def mergersFor(celf, thing, _default={}): 

81 typ = type(thing) 

82 

83 for celf in celf.mro(): 

84 mergers = getattr(celf, "mergers", None) 

85 if mergers is None: 

86 break 

87 

88 m = celf.mergers.get(typ, None) 

89 if m is not None: 

90 return m 

91 

92 return _default 

93 

94 def mergeObjects(self, out, lst, exclude=()): 

95 if hasattr(out, "ensureDecompiled"): 

96 out.ensureDecompiled(recurse=False) 

97 for item in lst: 

98 if hasattr(item, "ensureDecompiled"): 

99 item.ensureDecompiled(recurse=False) 

100 keys = sorted(vars(out).keys()) 

101 if not all(keys == sorted(vars(v).keys()) for v in lst): 

102 raise KeysDiffer( 

103 self, expected=keys, got=[sorted(vars(v).keys()) for v in lst] 

104 ) 

105 mergers = self.mergersFor(out) 

106 defaultMerger = mergers.get("*", self.__class__.mergeThings) 

107 try: 

108 for key in keys: 

109 if key in exclude: 

110 continue 

111 value = getattr(out, key) 

112 values = [getattr(table, key) for table in lst] 

113 mergerFunc = mergers.get(key, defaultMerger) 

114 mergerFunc(self, value, values) 

115 except VarLibMergeError as e: 

116 e.stack.append("." + key) 

117 raise 

118 

119 def mergeLists(self, out, lst): 

120 if not allEqualTo(out, lst, len): 

121 raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst]) 

122 for i, (value, values) in enumerate(zip(out, zip(*lst))): 

123 try: 

124 self.mergeThings(value, values) 

125 except VarLibMergeError as e: 

126 e.stack.append("[%d]" % i) 

127 raise 

128 

129 def mergeThings(self, out, lst): 

130 if not allEqualTo(out, lst, type): 

131 raise MismatchedTypes( 

132 self, expected=type(out).__name__, got=[type(x).__name__ for x in lst] 

133 ) 

134 mergerFunc = self.mergersFor(out).get(None, None) 

135 if mergerFunc is not None: 

136 mergerFunc(self, out, lst) 

137 elif isinstance(out, enum.Enum): 

138 # need to special-case Enums as have __dict__ but are not regular 'objects', 

139 # otherwise mergeObjects/mergeThings get trapped in a RecursionError 

140 if not allEqualTo(out, lst): 

141 raise ShouldBeConstant(self, expected=out, got=lst) 

142 elif hasattr(out, "__dict__"): 

143 self.mergeObjects(out, lst) 

144 elif isinstance(out, list): 

145 self.mergeLists(out, lst) 

146 else: 

147 if not allEqualTo(out, lst): 

148 raise ShouldBeConstant(self, expected=out, got=lst) 

149 

150 def mergeTables(self, font, master_ttfs, tableTags): 

151 for tag in tableTags: 

152 if tag not in font: 

153 continue 

154 try: 

155 self.ttfs = master_ttfs 

156 self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs]) 

157 except VarLibMergeError as e: 

158 e.stack.append(tag) 

159 raise 

160 

161 

162# 

163# Aligning merger 

164# 

165class AligningMerger(Merger): 

166 pass 

167 

168 

169@AligningMerger.merger(ot.GDEF, "GlyphClassDef") 

170def merge(merger, self, lst): 

171 if self is None: 

172 if not allNone(lst): 

173 raise NotANone(merger, expected=None, got=lst) 

174 return 

175 

176 lst = [l.classDefs for l in lst] 

177 self.classDefs = {} 

178 # We only care about the .classDefs 

179 self = self.classDefs 

180 

181 allKeys = set() 

182 allKeys.update(*[l.keys() for l in lst]) 

183 for k in allKeys: 

184 allValues = nonNone(l.get(k) for l in lst) 

185 if not allEqual(allValues): 

186 raise ShouldBeConstant( 

187 merger, expected=allValues[0], got=lst, stack=["." + k] 

188 ) 

189 if not allValues: 

190 self[k] = None 

191 else: 

192 self[k] = allValues[0] 

193 

194 

195def _SinglePosUpgradeToFormat2(self): 

196 if self.Format == 2: 

197 return self 

198 

199 ret = ot.SinglePos() 

200 ret.Format = 2 

201 ret.Coverage = self.Coverage 

202 ret.ValueFormat = self.ValueFormat 

203 ret.Value = [self.Value for _ in ret.Coverage.glyphs] 

204 ret.ValueCount = len(ret.Value) 

205 

206 return ret 

207 

208 

209def _merge_GlyphOrders(font, lst, values_lst=None, default=None): 

210 """Takes font and list of glyph lists (must be sorted by glyph id), and returns 

211 two things: 

212 - Combined glyph list, 

213 - If values_lst is None, return input glyph lists, but padded with None when a glyph 

214 was missing in a list. Otherwise, return values_lst list-of-list, padded with None 

215 to match combined glyph lists. 

216 """ 

217 if values_lst is None: 

218 dict_sets = [set(l) for l in lst] 

219 else: 

220 dict_sets = [{g: v for g, v in zip(l, vs)} for l, vs in zip(lst, values_lst)] 

221 combined = set() 

222 combined.update(*dict_sets) 

223 

224 sortKey = font.getReverseGlyphMap().__getitem__ 

225 order = sorted(combined, key=sortKey) 

226 # Make sure all input glyphsets were in proper order 

227 if not all(sorted(vs, key=sortKey) == vs for vs in lst): 

228 raise InconsistentGlyphOrder() 

229 del combined 

230 

231 paddedValues = None 

232 if values_lst is None: 

233 padded = [ 

234 [glyph if glyph in dict_set else default for glyph in order] 

235 for dict_set in dict_sets 

236 ] 

237 else: 

238 assert len(lst) == len(values_lst) 

239 padded = [ 

240 [dict_set[glyph] if glyph in dict_set else default for glyph in order] 

241 for dict_set in dict_sets 

242 ] 

243 return order, padded 

244 

245 

246@AligningMerger.merger(otBase.ValueRecord) 

247def merge(merger, self, lst): 

248 # Code below sometimes calls us with self being 

249 # a new object. Copy it from lst and recurse. 

250 self.__dict__ = lst[0].__dict__.copy() 

251 merger.mergeObjects(self, lst) 

252 

253 

254@AligningMerger.merger(ot.Anchor) 

255def merge(merger, self, lst): 

256 # Code below sometimes calls us with self being 

257 # a new object. Copy it from lst and recurse. 

258 self.__dict__ = lst[0].__dict__.copy() 

259 merger.mergeObjects(self, lst) 

260 

261 

262def _Lookup_SinglePos_get_effective_value(merger, subtables, glyph): 

263 for self in subtables: 

264 if ( 

265 self is None 

266 or type(self) != ot.SinglePos 

267 or self.Coverage is None 

268 or glyph not in self.Coverage.glyphs 

269 ): 

270 continue 

271 if self.Format == 1: 

272 return self.Value 

273 elif self.Format == 2: 

274 return self.Value[self.Coverage.glyphs.index(glyph)] 

275 else: 

276 raise UnsupportedFormat(merger, subtable="single positioning lookup") 

277 return None 

278 

279 

280def _Lookup_PairPos_get_effective_value_pair( 

281 merger, subtables, firstGlyph, secondGlyph 

282): 

283 for self in subtables: 

284 if ( 

285 self is None 

286 or type(self) != ot.PairPos 

287 or self.Coverage is None 

288 or firstGlyph not in self.Coverage.glyphs 

289 ): 

290 continue 

291 if self.Format == 1: 

292 ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)] 

293 pvr = ps.PairValueRecord 

294 for rec in pvr: # TODO Speed up 

295 if rec.SecondGlyph == secondGlyph: 

296 return rec 

297 continue 

298 elif self.Format == 2: 

299 klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0) 

300 klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0) 

301 return self.Class1Record[klass1].Class2Record[klass2] 

302 else: 

303 raise UnsupportedFormat(merger, subtable="pair positioning lookup") 

304 return None 

305 

306 

307@AligningMerger.merger(ot.SinglePos) 

308def merge(merger, self, lst): 

309 self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0) 

310 if not (len(lst) == 1 or (valueFormat & ~0xF == 0)): 

311 raise UnsupportedFormat(merger, subtable="single positioning lookup") 

312 

313 # If all have same coverage table and all are format 1, 

314 coverageGlyphs = self.Coverage.glyphs 

315 if all(v.Format == 1 for v in lst) and all( 

316 coverageGlyphs == v.Coverage.glyphs for v in lst 

317 ): 

318 self.Value = otBase.ValueRecord(valueFormat, self.Value) 

319 if valueFormat != 0: 

320 # If v.Value is None, it means a kerning of 0; we want 

321 # it to participate in the model still. 

322 # https://github.com/fonttools/fonttools/issues/3111 

323 merger.mergeThings( 

324 self.Value, 

325 [v.Value if v.Value is not None else otBase.ValueRecord() for v in lst], 

326 ) 

327 self.ValueFormat = self.Value.getFormat() 

328 return 

329 

330 # Upgrade everything to Format=2 

331 self.Format = 2 

332 lst = [_SinglePosUpgradeToFormat2(v) for v in lst] 

333 

334 # Align them 

335 glyphs, padded = _merge_GlyphOrders( 

336 merger.font, [v.Coverage.glyphs for v in lst], [v.Value for v in lst] 

337 ) 

338 

339 self.Coverage.glyphs = glyphs 

340 self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs] 

341 self.ValueCount = len(self.Value) 

342 

343 for i, values in enumerate(padded): 

344 for j, glyph in enumerate(glyphs): 

345 if values[j] is not None: 

346 continue 

347 # Fill in value from other subtables 

348 # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness 

349 # is different between used subtable and current subtable! 

350 # TODO(behdad) Check and warn if that happens? 

351 v = _Lookup_SinglePos_get_effective_value( 

352 merger, merger.lookup_subtables[i], glyph 

353 ) 

354 if v is None: 

355 v = otBase.ValueRecord(valueFormat) 

356 values[j] = v 

357 

358 merger.mergeLists(self.Value, padded) 

359 

360 # Merge everything else; though, there shouldn't be anything else. :) 

361 merger.mergeObjects( 

362 self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat") 

363 ) 

364 self.ValueFormat = reduce( 

365 int.__or__, [v.getEffectiveFormat() for v in self.Value], 0 

366 ) 

367 

368 

369@AligningMerger.merger(ot.PairSet) 

370def merge(merger, self, lst): 

371 # Align them 

372 glyphs, padded = _merge_GlyphOrders( 

373 merger.font, 

374 [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], 

375 [vs.PairValueRecord for vs in lst], 

376 ) 

377 

378 self.PairValueRecord = pvrs = [] 

379 for glyph in glyphs: 

380 pvr = ot.PairValueRecord() 

381 pvr.SecondGlyph = glyph 

382 pvr.Value1 = ( 

383 otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None 

384 ) 

385 pvr.Value2 = ( 

386 otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None 

387 ) 

388 pvrs.append(pvr) 

389 self.PairValueCount = len(self.PairValueRecord) 

390 

391 for i, values in enumerate(padded): 

392 for j, glyph in enumerate(glyphs): 

393 # Fill in value from other subtables 

394 v = ot.PairValueRecord() 

395 v.SecondGlyph = glyph 

396 if values[j] is not None: 

397 vpair = values[j] 

398 else: 

399 vpair = _Lookup_PairPos_get_effective_value_pair( 

400 merger, merger.lookup_subtables[i], self._firstGlyph, glyph 

401 ) 

402 if vpair is None: 

403 v1, v2 = None, None 

404 else: 

405 v1 = getattr(vpair, "Value1", None) 

406 v2 = getattr(vpair, "Value2", None) 

407 v.Value1 = ( 

408 otBase.ValueRecord(merger.valueFormat1, src=v1) 

409 if merger.valueFormat1 

410 else None 

411 ) 

412 v.Value2 = ( 

413 otBase.ValueRecord(merger.valueFormat2, src=v2) 

414 if merger.valueFormat2 

415 else None 

416 ) 

417 values[j] = v 

418 del self._firstGlyph 

419 

420 merger.mergeLists(self.PairValueRecord, padded) 

421 

422 

423def _PairPosFormat1_merge(self, lst, merger): 

424 assert allEqual( 

425 [l.ValueFormat2 == 0 for l in lst if l.PairSet] 

426 ), "Report bug against fonttools." 

427 

428 # Merge everything else; makes sure Format is the same. 

429 merger.mergeObjects( 

430 self, 

431 lst, 

432 exclude=("Coverage", "PairSet", "PairSetCount", "ValueFormat1", "ValueFormat2"), 

433 ) 

434 

435 empty = ot.PairSet() 

436 empty.PairValueRecord = [] 

437 empty.PairValueCount = 0 

438 

439 # Align them 

440 glyphs, padded = _merge_GlyphOrders( 

441 merger.font, 

442 [v.Coverage.glyphs for v in lst], 

443 [v.PairSet for v in lst], 

444 default=empty, 

445 ) 

446 

447 self.Coverage.glyphs = glyphs 

448 self.PairSet = [ot.PairSet() for _ in glyphs] 

449 self.PairSetCount = len(self.PairSet) 

450 for glyph, ps in zip(glyphs, self.PairSet): 

451 ps._firstGlyph = glyph 

452 

453 merger.mergeLists(self.PairSet, padded) 

454 

455 

456def _ClassDef_invert(self, allGlyphs=None): 

457 if isinstance(self, dict): 

458 classDefs = self 

459 else: 

460 classDefs = self.classDefs if self and self.classDefs else {} 

461 m = max(classDefs.values()) if classDefs else 0 

462 

463 ret = [] 

464 for _ in range(m + 1): 

465 ret.append(set()) 

466 

467 for k, v in classDefs.items(): 

468 ret[v].add(k) 

469 

470 # Class-0 is special. It's "everything else". 

471 if allGlyphs is None: 

472 ret[0] = None 

473 else: 

474 # Limit all classes to glyphs in allGlyphs. 

475 # Collect anything without a non-zero class into class=zero. 

476 ret[0] = class0 = set(allGlyphs) 

477 for s in ret[1:]: 

478 s.intersection_update(class0) 

479 class0.difference_update(s) 

480 

481 return ret 

482 

483 

484def _ClassDef_merge_classify(lst, allGlyphses=None): 

485 self = ot.ClassDef() 

486 self.classDefs = classDefs = {} 

487 allGlyphsesWasNone = allGlyphses is None 

488 if allGlyphsesWasNone: 

489 allGlyphses = [None] * len(lst) 

490 

491 classifier = classifyTools.Classifier() 

492 for classDef, allGlyphs in zip(lst, allGlyphses): 

493 sets = _ClassDef_invert(classDef, allGlyphs) 

494 if allGlyphs is None: 

495 sets = sets[1:] 

496 classifier.update(sets) 

497 classes = classifier.getClasses() 

498 

499 if allGlyphsesWasNone: 

500 classes.insert(0, set()) 

501 

502 for i, classSet in enumerate(classes): 

503 if i == 0: 

504 continue 

505 for g in classSet: 

506 classDefs[g] = i 

507 

508 return self, classes 

509 

510 

511def _PairPosFormat2_align_matrices(self, lst, font, transparent=False): 

512 matrices = [l.Class1Record for l in lst] 

513 

514 # Align first classes 

515 self.ClassDef1, classes = _ClassDef_merge_classify( 

516 [l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst] 

517 ) 

518 self.Class1Count = len(classes) 

519 new_matrices = [] 

520 for l, matrix in zip(lst, matrices): 

521 nullRow = None 

522 coverage = set(l.Coverage.glyphs) 

523 classDef1 = l.ClassDef1.classDefs 

524 class1Records = [] 

525 for classSet in classes: 

526 exemplarGlyph = next(iter(classSet)) 

527 if exemplarGlyph not in coverage: 

528 # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f, 

529 # Fixes https://github.com/googlei18n/fontmake/issues/470 

530 # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9 

531 # when merger becomes selfless. 

532 nullRow = None 

533 if nullRow is None: 

534 nullRow = ot.Class1Record() 

535 class2records = nullRow.Class2Record = [] 

536 # TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f 

537 for _ in range(l.Class2Count): 

538 if transparent: 

539 rec2 = None 

540 else: 

541 rec2 = ot.Class2Record() 

542 rec2.Value1 = ( 

543 otBase.ValueRecord(self.ValueFormat1) 

544 if self.ValueFormat1 

545 else None 

546 ) 

547 rec2.Value2 = ( 

548 otBase.ValueRecord(self.ValueFormat2) 

549 if self.ValueFormat2 

550 else None 

551 ) 

552 class2records.append(rec2) 

553 rec1 = nullRow 

554 else: 

555 klass = classDef1.get(exemplarGlyph, 0) 

556 rec1 = matrix[klass] # TODO handle out-of-range? 

557 class1Records.append(rec1) 

558 new_matrices.append(class1Records) 

559 matrices = new_matrices 

560 del new_matrices 

561 

562 # Align second classes 

563 self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst]) 

564 self.Class2Count = len(classes) 

565 new_matrices = [] 

566 for l, matrix in zip(lst, matrices): 

567 classDef2 = l.ClassDef2.classDefs 

568 class1Records = [] 

569 for rec1old in matrix: 

570 oldClass2Records = rec1old.Class2Record 

571 rec1new = ot.Class1Record() 

572 class2Records = rec1new.Class2Record = [] 

573 for classSet in classes: 

574 if not classSet: # class=0 

575 rec2 = oldClass2Records[0] 

576 else: 

577 exemplarGlyph = next(iter(classSet)) 

578 klass = classDef2.get(exemplarGlyph, 0) 

579 rec2 = oldClass2Records[klass] 

580 class2Records.append(copy.deepcopy(rec2)) 

581 class1Records.append(rec1new) 

582 new_matrices.append(class1Records) 

583 matrices = new_matrices 

584 del new_matrices 

585 

586 return matrices 

587 

588 

589def _PairPosFormat2_merge(self, lst, merger): 

590 assert allEqual( 

591 [l.ValueFormat2 == 0 for l in lst if l.Class1Record] 

592 ), "Report bug against fonttools." 

593 

594 merger.mergeObjects( 

595 self, 

596 lst, 

597 exclude=( 

598 "Coverage", 

599 "ClassDef1", 

600 "Class1Count", 

601 "ClassDef2", 

602 "Class2Count", 

603 "Class1Record", 

604 "ValueFormat1", 

605 "ValueFormat2", 

606 ), 

607 ) 

608 

609 # Align coverages 

610 glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst]) 

611 self.Coverage.glyphs = glyphs 

612 

613 # Currently, if the coverage of PairPosFormat2 subtables are different, 

614 # we do NOT bother walking down the subtable list when filling in new 

615 # rows for alignment. As such, this is only correct if current subtable 

616 # is the last subtable in the lookup. Ensure that. 

617 # 

618 # Note that our canonicalization process merges trailing PairPosFormat2's, 

619 # so in reality this is rare. 

620 for l, subtables in zip(lst, merger.lookup_subtables): 

621 if l.Coverage.glyphs != glyphs: 

622 assert l == subtables[-1] 

623 

624 matrices = _PairPosFormat2_align_matrices(self, lst, merger.font) 

625 

626 self.Class1Record = list(matrices[0]) # TODO move merger to be selfless 

627 merger.mergeLists(self.Class1Record, matrices) 

628 

629 

630@AligningMerger.merger(ot.PairPos) 

631def merge(merger, self, lst): 

632 merger.valueFormat1 = self.ValueFormat1 = reduce( 

633 int.__or__, [l.ValueFormat1 for l in lst], 0 

634 ) 

635 merger.valueFormat2 = self.ValueFormat2 = reduce( 

636 int.__or__, [l.ValueFormat2 for l in lst], 0 

637 ) 

638 

639 if self.Format == 1: 

640 _PairPosFormat1_merge(self, lst, merger) 

641 elif self.Format == 2: 

642 _PairPosFormat2_merge(self, lst, merger) 

643 else: 

644 raise UnsupportedFormat(merger, subtable="pair positioning lookup") 

645 

646 del merger.valueFormat1, merger.valueFormat2 

647 

648 # Now examine the list of value records, and update to the union of format values, 

649 # as merge might have created new values. 

650 vf1 = 0 

651 vf2 = 0 

652 if self.Format == 1: 

653 for pairSet in self.PairSet: 

654 for pairValueRecord in pairSet.PairValueRecord: 

655 pv1 = getattr(pairValueRecord, "Value1", None) 

656 if pv1 is not None: 

657 vf1 |= pv1.getFormat() 

658 pv2 = getattr(pairValueRecord, "Value2", None) 

659 if pv2 is not None: 

660 vf2 |= pv2.getFormat() 

661 elif self.Format == 2: 

662 for class1Record in self.Class1Record: 

663 for class2Record in class1Record.Class2Record: 

664 pv1 = getattr(class2Record, "Value1", None) 

665 if pv1 is not None: 

666 vf1 |= pv1.getFormat() 

667 pv2 = getattr(class2Record, "Value2", None) 

668 if pv2 is not None: 

669 vf2 |= pv2.getFormat() 

670 self.ValueFormat1 = vf1 

671 self.ValueFormat2 = vf2 

672 

673 

674def _MarkBasePosFormat1_merge(self, lst, merger, Mark="Mark", Base="Base"): 

675 self.ClassCount = max(l.ClassCount for l in lst) 

676 

677 MarkCoverageGlyphs, MarkRecords = _merge_GlyphOrders( 

678 merger.font, 

679 [getattr(l, Mark + "Coverage").glyphs for l in lst], 

680 [getattr(l, Mark + "Array").MarkRecord for l in lst], 

681 ) 

682 getattr(self, Mark + "Coverage").glyphs = MarkCoverageGlyphs 

683 

684 BaseCoverageGlyphs, BaseRecords = _merge_GlyphOrders( 

685 merger.font, 

686 [getattr(l, Base + "Coverage").glyphs for l in lst], 

687 [getattr(getattr(l, Base + "Array"), Base + "Record") for l in lst], 

688 ) 

689 getattr(self, Base + "Coverage").glyphs = BaseCoverageGlyphs 

690 

691 # MarkArray 

692 records = [] 

693 for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)): 

694 allClasses = [r.Class for r in glyphRecords if r is not None] 

695 

696 # TODO Right now we require that all marks have same class in 

697 # all masters that cover them. This is not required. 

698 # 

699 # We can relax that by just requiring that all marks that have 

700 # the same class in a master, have the same class in every other 

701 # master. Indeed, if, say, a sparse master only covers one mark, 

702 # that mark probably will get class 0, which would possibly be 

703 # different from its class in other masters. 

704 # 

705 # We can even go further and reclassify marks to support any 

706 # input. But, since, it's unlikely that two marks being both, 

707 # say, "top" in one master, and one being "top" and other being 

708 # "top-right" in another master, we shouldn't do that, as any 

709 # failures in that case will probably signify mistakes in the 

710 # input masters. 

711 

712 if not allEqual(allClasses): 

713 raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses) 

714 else: 

715 rec = ot.MarkRecord() 

716 rec.Class = allClasses[0] 

717 allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords] 

718 if allNone(allAnchors): 

719 anchor = None 

720 else: 

721 anchor = ot.Anchor() 

722 anchor.Format = 1 

723 merger.mergeThings(anchor, allAnchors) 

724 rec.MarkAnchor = anchor 

725 records.append(rec) 

726 array = ot.MarkArray() 

727 array.MarkRecord = records 

728 array.MarkCount = len(records) 

729 setattr(self, Mark + "Array", array) 

730 

731 # BaseArray 

732 records = [] 

733 for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)): 

734 if allNone(glyphRecords): 

735 rec = None 

736 else: 

737 rec = getattr(ot, Base + "Record")() 

738 anchors = [] 

739 setattr(rec, Base + "Anchor", anchors) 

740 glyphAnchors = [ 

741 [] if r is None else getattr(r, Base + "Anchor") for r in glyphRecords 

742 ] 

743 for l in glyphAnchors: 

744 l.extend([None] * (self.ClassCount - len(l))) 

745 for allAnchors in zip(*glyphAnchors): 

746 if allNone(allAnchors): 

747 anchor = None 

748 else: 

749 anchor = ot.Anchor() 

750 anchor.Format = 1 

751 merger.mergeThings(anchor, allAnchors) 

752 anchors.append(anchor) 

753 records.append(rec) 

754 array = getattr(ot, Base + "Array")() 

755 setattr(array, Base + "Record", records) 

756 setattr(array, Base + "Count", len(records)) 

757 setattr(self, Base + "Array", array) 

758 

759 

760@AligningMerger.merger(ot.MarkBasePos) 

761def merge(merger, self, lst): 

762 if not allEqualTo(self.Format, (l.Format for l in lst)): 

763 raise InconsistentFormats( 

764 merger, 

765 subtable="mark-to-base positioning lookup", 

766 expected=self.Format, 

767 got=[l.Format for l in lst], 

768 ) 

769 if self.Format == 1: 

770 _MarkBasePosFormat1_merge(self, lst, merger) 

771 else: 

772 raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup") 

773 

774 

775@AligningMerger.merger(ot.MarkMarkPos) 

776def merge(merger, self, lst): 

777 if not allEqualTo(self.Format, (l.Format for l in lst)): 

778 raise InconsistentFormats( 

779 merger, 

780 subtable="mark-to-mark positioning lookup", 

781 expected=self.Format, 

782 got=[l.Format for l in lst], 

783 ) 

784 if self.Format == 1: 

785 _MarkBasePosFormat1_merge(self, lst, merger, "Mark1", "Mark2") 

786 else: 

787 raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup") 

788 

789 

790def _PairSet_flatten(lst, font): 

791 self = ot.PairSet() 

792 self.Coverage = ot.Coverage() 

793 

794 # Align them 

795 glyphs, padded = _merge_GlyphOrders( 

796 font, 

797 [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], 

798 [vs.PairValueRecord for vs in lst], 

799 ) 

800 

801 self.Coverage.glyphs = glyphs 

802 self.PairValueRecord = pvrs = [] 

803 for values in zip(*padded): 

804 for v in values: 

805 if v is not None: 

806 pvrs.append(v) 

807 break 

808 else: 

809 assert False 

810 self.PairValueCount = len(self.PairValueRecord) 

811 

812 return self 

813 

814 

815def _Lookup_PairPosFormat1_subtables_flatten(lst, font): 

816 assert allEqual( 

817 [l.ValueFormat2 == 0 for l in lst if l.PairSet] 

818 ), "Report bug against fonttools." 

819 

820 self = ot.PairPos() 

821 self.Format = 1 

822 self.Coverage = ot.Coverage() 

823 self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) 

824 self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) 

825 

826 # Align them 

827 glyphs, padded = _merge_GlyphOrders( 

828 font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst] 

829 ) 

830 

831 self.Coverage.glyphs = glyphs 

832 self.PairSet = [ 

833 _PairSet_flatten([v for v in values if v is not None], font) 

834 for values in zip(*padded) 

835 ] 

836 self.PairSetCount = len(self.PairSet) 

837 return self 

838 

839 

840def _Lookup_PairPosFormat2_subtables_flatten(lst, font): 

841 assert allEqual( 

842 [l.ValueFormat2 == 0 for l in lst if l.Class1Record] 

843 ), "Report bug against fonttools." 

844 

845 self = ot.PairPos() 

846 self.Format = 2 

847 self.Coverage = ot.Coverage() 

848 self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) 

849 self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) 

850 

851 # Align them 

852 glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst]) 

853 self.Coverage.glyphs = glyphs 

854 

855 matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True) 

856 

857 matrix = self.Class1Record = [] 

858 for rows in zip(*matrices): 

859 row = ot.Class1Record() 

860 matrix.append(row) 

861 row.Class2Record = [] 

862 row = row.Class2Record 

863 for cols in zip(*list(r.Class2Record for r in rows)): 

864 col = next(iter(c for c in cols if c is not None)) 

865 row.append(col) 

866 

867 return self 

868 

869 

870def _Lookup_PairPos_subtables_canonicalize(lst, font): 

871 """Merge multiple Format1 subtables at the beginning of lst, 

872 and merge multiple consecutive Format2 subtables that have the same 

873 Class2 (ie. were split because of offset overflows). Returns new list.""" 

874 lst = list(lst) 

875 

876 l = len(lst) 

877 i = 0 

878 while i < l and lst[i].Format == 1: 

879 i += 1 

880 lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)] 

881 

882 l = len(lst) 

883 i = l 

884 while i > 0 and lst[i - 1].Format == 2: 

885 i -= 1 

886 lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)] 

887 

888 return lst 

889 

890 

891def _Lookup_SinglePos_subtables_flatten(lst, font, min_inclusive_rec_format): 

892 glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst], None) 

893 num_glyphs = len(glyphs) 

894 new = ot.SinglePos() 

895 new.Format = 2 

896 new.ValueFormat = min_inclusive_rec_format 

897 new.Coverage = ot.Coverage() 

898 new.Coverage.glyphs = glyphs 

899 new.ValueCount = num_glyphs 

900 new.Value = [None] * num_glyphs 

901 for singlePos in lst: 

902 if singlePos.Format == 1: 

903 val_rec = singlePos.Value 

904 for gname in singlePos.Coverage.glyphs: 

905 i = glyphs.index(gname) 

906 new.Value[i] = copy.deepcopy(val_rec) 

907 elif singlePos.Format == 2: 

908 for j, gname in enumerate(singlePos.Coverage.glyphs): 

909 val_rec = singlePos.Value[j] 

910 i = glyphs.index(gname) 

911 new.Value[i] = copy.deepcopy(val_rec) 

912 return [new] 

913 

914 

915@AligningMerger.merger(ot.CursivePos) 

916def merge(merger, self, lst): 

917 # Align them 

918 glyphs, padded = _merge_GlyphOrders( 

919 merger.font, 

920 [l.Coverage.glyphs for l in lst], 

921 [l.EntryExitRecord for l in lst], 

922 ) 

923 

924 self.Format = 1 

925 self.Coverage = ot.Coverage() 

926 self.Coverage.glyphs = glyphs 

927 self.EntryExitRecord = [] 

928 for _ in glyphs: 

929 rec = ot.EntryExitRecord() 

930 rec.EntryAnchor = ot.Anchor() 

931 rec.EntryAnchor.Format = 1 

932 rec.ExitAnchor = ot.Anchor() 

933 rec.ExitAnchor.Format = 1 

934 self.EntryExitRecord.append(rec) 

935 merger.mergeLists(self.EntryExitRecord, padded) 

936 self.EntryExitCount = len(self.EntryExitRecord) 

937 

938 

939@AligningMerger.merger(ot.EntryExitRecord) 

940def merge(merger, self, lst): 

941 if all(master.EntryAnchor is None for master in lst): 

942 self.EntryAnchor = None 

943 if all(master.ExitAnchor is None for master in lst): 

944 self.ExitAnchor = None 

945 merger.mergeObjects(self, lst) 

946 

947 

948@AligningMerger.merger(ot.Lookup) 

949def merge(merger, self, lst): 

950 subtables = merger.lookup_subtables = [l.SubTable for l in lst] 

951 

952 # Remove Extension subtables 

953 for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]: 

954 if not sts: 

955 continue 

956 if sts[0].__class__.__name__.startswith("Extension"): 

957 if not allEqual([st.__class__ for st in sts]): 

958 raise InconsistentExtensions( 

959 merger, 

960 expected="Extension", 

961 got=[st.__class__.__name__ for st in sts], 

962 ) 

963 if not allEqual([st.ExtensionLookupType for st in sts]): 

964 raise InconsistentExtensions(merger) 

965 l.LookupType = sts[0].ExtensionLookupType 

966 new_sts = [st.ExtSubTable for st in sts] 

967 del sts[:] 

968 sts.extend(new_sts) 

969 

970 isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos) 

971 

972 if isPairPos: 

973 # AFDKO and feaLib sometimes generate two Format1 subtables instead of one. 

974 # Merge those before continuing. 

975 # https://github.com/fonttools/fonttools/issues/719 

976 self.SubTable = _Lookup_PairPos_subtables_canonicalize( 

977 self.SubTable, merger.font 

978 ) 

979 subtables = merger.lookup_subtables = [ 

980 _Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables 

981 ] 

982 else: 

983 isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos) 

984 if isSinglePos: 

985 numSubtables = [len(st) for st in subtables] 

986 if not all([nums == numSubtables[0] for nums in numSubtables]): 

987 # Flatten list of SinglePos subtables to single Format 2 subtable, 

988 # with all value records set to the rec format type. 

989 # We use buildSinglePos() to optimize the lookup after merging. 

990 valueFormatList = [t.ValueFormat for st in subtables for t in st] 

991 # Find the minimum value record that can accomodate all the singlePos subtables. 

992 mirf = reduce(ior, valueFormatList) 

993 self.SubTable = _Lookup_SinglePos_subtables_flatten( 

994 self.SubTable, merger.font, mirf 

995 ) 

996 subtables = merger.lookup_subtables = [ 

997 _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf) 

998 for st in subtables 

999 ] 

1000 flattened = True 

1001 else: 

1002 flattened = False 

1003 

1004 merger.mergeLists(self.SubTable, subtables) 

1005 self.SubTableCount = len(self.SubTable) 

1006 

1007 if isPairPos: 

1008 # If format-1 subtable created during canonicalization is empty, remove it. 

1009 assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1 

1010 if not self.SubTable[0].Coverage.glyphs: 

1011 self.SubTable.pop(0) 

1012 self.SubTableCount -= 1 

1013 

1014 # If format-2 subtable created during canonicalization is empty, remove it. 

1015 assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2 

1016 if not self.SubTable[-1].Coverage.glyphs: 

1017 self.SubTable.pop(-1) 

1018 self.SubTableCount -= 1 

1019 

1020 # Compact the merged subtables 

1021 # This is a good moment to do it because the compaction should create 

1022 # smaller subtables, which may prevent overflows from happening. 

1023 # Keep reading the value from the ENV until ufo2ft switches to the config system 

1024 level = merger.font.cfg.get( 

1025 "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", 

1026 default=_compression_level_from_env(), 

1027 ) 

1028 if level != 0: 

1029 log.info("Compacting GPOS...") 

1030 self.SubTable = compact_pair_pos(merger.font, level, self.SubTable) 

1031 self.SubTableCount = len(self.SubTable) 

1032 

1033 elif isSinglePos and flattened: 

1034 singlePosTable = self.SubTable[0] 

1035 glyphs = singlePosTable.Coverage.glyphs 

1036 # We know that singlePosTable is Format 2, as this is set 

1037 # in _Lookup_SinglePos_subtables_flatten. 

1038 singlePosMapping = { 

1039 gname: valRecord for gname, valRecord in zip(glyphs, singlePosTable.Value) 

1040 } 

1041 self.SubTable = buildSinglePos( 

1042 singlePosMapping, merger.font.getReverseGlyphMap() 

1043 ) 

1044 merger.mergeObjects(self, lst, exclude=["SubTable", "SubTableCount"]) 

1045 

1046 del merger.lookup_subtables 

1047 

1048 

1049# 

1050# InstancerMerger 

1051# 

1052 

1053 

1054class InstancerMerger(AligningMerger): 

1055 """A merger that takes multiple master fonts, and instantiates 

1056 an instance.""" 

1057 

1058 def __init__(self, font, model, location): 

1059 Merger.__init__(self, font) 

1060 self.model = model 

1061 self.location = location 

1062 self.scalars = model.getScalars(location) 

1063 

1064 

1065@InstancerMerger.merger(ot.CaretValue) 

1066def merge(merger, self, lst): 

1067 assert self.Format == 1 

1068 Coords = [a.Coordinate for a in lst] 

1069 model = merger.model 

1070 scalars = merger.scalars 

1071 self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars)) 

1072 

1073 

1074@InstancerMerger.merger(ot.Anchor) 

1075def merge(merger, self, lst): 

1076 assert self.Format == 1 

1077 XCoords = [a.XCoordinate for a in lst] 

1078 YCoords = [a.YCoordinate for a in lst] 

1079 model = merger.model 

1080 scalars = merger.scalars 

1081 self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars)) 

1082 self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars)) 

1083 

1084 

1085@InstancerMerger.merger(otBase.ValueRecord) 

1086def merge(merger, self, lst): 

1087 model = merger.model 

1088 scalars = merger.scalars 

1089 # TODO Handle differing valueformats 

1090 for name, tableName in [ 

1091 ("XAdvance", "XAdvDevice"), 

1092 ("YAdvance", "YAdvDevice"), 

1093 ("XPlacement", "XPlaDevice"), 

1094 ("YPlacement", "YPlaDevice"), 

1095 ]: 

1096 assert not hasattr(self, tableName) 

1097 

1098 if hasattr(self, name): 

1099 values = [getattr(a, name, 0) for a in lst] 

1100 value = otRound(model.interpolateFromMastersAndScalars(values, scalars)) 

1101 setattr(self, name, value) 

1102 

1103 

1104# 

1105# MutatorMerger 

1106# 

1107 

1108 

1109class MutatorMerger(AligningMerger): 

1110 """A merger that takes a variable font, and instantiates 

1111 an instance. While there's no "merging" to be done per se, 

1112 the operation can benefit from many operations that the 

1113 aligning merger does.""" 

1114 

1115 def __init__(self, font, instancer, deleteVariations=True): 

1116 Merger.__init__(self, font) 

1117 self.instancer = instancer 

1118 self.deleteVariations = deleteVariations 

1119 

1120 

1121@MutatorMerger.merger(ot.CaretValue) 

1122def merge(merger, self, lst): 

1123 # Hack till we become selfless. 

1124 self.__dict__ = lst[0].__dict__.copy() 

1125 

1126 if self.Format != 3: 

1127 return 

1128 

1129 instancer = merger.instancer 

1130 dev = self.DeviceTable 

1131 if merger.deleteVariations: 

1132 del self.DeviceTable 

1133 if dev: 

1134 assert dev.DeltaFormat == 0x8000 

1135 varidx = (dev.StartSize << 16) + dev.EndSize 

1136 delta = otRound(instancer[varidx]) 

1137 self.Coordinate += delta 

1138 

1139 if merger.deleteVariations: 

1140 self.Format = 1 

1141 

1142 

1143@MutatorMerger.merger(ot.Anchor) 

1144def merge(merger, self, lst): 

1145 # Hack till we become selfless. 

1146 self.__dict__ = lst[0].__dict__.copy() 

1147 

1148 if self.Format != 3: 

1149 return 

1150 

1151 instancer = merger.instancer 

1152 for v in "XY": 

1153 tableName = v + "DeviceTable" 

1154 if not hasattr(self, tableName): 

1155 continue 

1156 dev = getattr(self, tableName) 

1157 if merger.deleteVariations: 

1158 delattr(self, tableName) 

1159 if dev is None: 

1160 continue 

1161 

1162 assert dev.DeltaFormat == 0x8000 

1163 varidx = (dev.StartSize << 16) + dev.EndSize 

1164 delta = otRound(instancer[varidx]) 

1165 

1166 attr = v + "Coordinate" 

1167 setattr(self, attr, getattr(self, attr) + delta) 

1168 

1169 if merger.deleteVariations: 

1170 self.Format = 1 

1171 

1172 

1173@MutatorMerger.merger(otBase.ValueRecord) 

1174def merge(merger, self, lst): 

1175 # Hack till we become selfless. 

1176 self.__dict__ = lst[0].__dict__.copy() 

1177 

1178 instancer = merger.instancer 

1179 for name, tableName in [ 

1180 ("XAdvance", "XAdvDevice"), 

1181 ("YAdvance", "YAdvDevice"), 

1182 ("XPlacement", "XPlaDevice"), 

1183 ("YPlacement", "YPlaDevice"), 

1184 ]: 

1185 if not hasattr(self, tableName): 

1186 continue 

1187 dev = getattr(self, tableName) 

1188 if merger.deleteVariations: 

1189 delattr(self, tableName) 

1190 if dev is None: 

1191 continue 

1192 

1193 assert dev.DeltaFormat == 0x8000 

1194 varidx = (dev.StartSize << 16) + dev.EndSize 

1195 delta = otRound(instancer[varidx]) 

1196 

1197 setattr(self, name, getattr(self, name, 0) + delta) 

1198 

1199 

1200# 

1201# VariationMerger 

1202# 

1203 

1204 

1205class VariationMerger(AligningMerger): 

1206 """A merger that takes multiple master fonts, and builds a 

1207 variable font.""" 

1208 

1209 def __init__(self, model, axisTags, font): 

1210 Merger.__init__(self, font) 

1211 self.store_builder = varStore.OnlineVarStoreBuilder(axisTags) 

1212 self.setModel(model) 

1213 

1214 def setModel(self, model): 

1215 self.model = model 

1216 self.store_builder.setModel(model) 

1217 

1218 def mergeThings(self, out, lst): 

1219 masterModel = None 

1220 origTTFs = None 

1221 if None in lst: 

1222 if allNone(lst): 

1223 if out is not None: 

1224 raise FoundANone(self, got=lst) 

1225 return 

1226 

1227 # temporarily subset the list of master ttfs to the ones for which 

1228 # master values are not None 

1229 origTTFs = self.ttfs 

1230 if self.ttfs: 

1231 self.ttfs = subList([v is not None for v in lst], self.ttfs) 

1232 

1233 masterModel = self.model 

1234 model, lst = masterModel.getSubModel(lst) 

1235 self.setModel(model) 

1236 

1237 super(VariationMerger, self).mergeThings(out, lst) 

1238 

1239 if masterModel: 

1240 self.setModel(masterModel) 

1241 if origTTFs: 

1242 self.ttfs = origTTFs 

1243 

1244 

1245def buildVarDevTable(store_builder, master_values): 

1246 if allEqual(master_values): 

1247 return master_values[0], None 

1248 base, varIdx = store_builder.storeMasters(master_values) 

1249 return base, builder.buildVarDevTable(varIdx) 

1250 

1251 

1252@VariationMerger.merger(ot.BaseCoord) 

1253def merge(merger, self, lst): 

1254 if self.Format != 1: 

1255 raise UnsupportedFormat(merger, subtable="a baseline coordinate") 

1256 self.Coordinate, DeviceTable = buildVarDevTable( 

1257 merger.store_builder, [a.Coordinate for a in lst] 

1258 ) 

1259 if DeviceTable: 

1260 self.Format = 3 

1261 self.DeviceTable = DeviceTable 

1262 

1263 

1264@VariationMerger.merger(ot.CaretValue) 

1265def merge(merger, self, lst): 

1266 if self.Format != 1: 

1267 raise UnsupportedFormat(merger, subtable="a caret") 

1268 self.Coordinate, DeviceTable = buildVarDevTable( 

1269 merger.store_builder, [a.Coordinate for a in lst] 

1270 ) 

1271 if DeviceTable: 

1272 self.Format = 3 

1273 self.DeviceTable = DeviceTable 

1274 

1275 

1276@VariationMerger.merger(ot.Anchor) 

1277def merge(merger, self, lst): 

1278 if self.Format != 1: 

1279 raise UnsupportedFormat(merger, subtable="an anchor") 

1280 self.XCoordinate, XDeviceTable = buildVarDevTable( 

1281 merger.store_builder, [a.XCoordinate for a in lst] 

1282 ) 

1283 self.YCoordinate, YDeviceTable = buildVarDevTable( 

1284 merger.store_builder, [a.YCoordinate for a in lst] 

1285 ) 

1286 if XDeviceTable or YDeviceTable: 

1287 self.Format = 3 

1288 self.XDeviceTable = XDeviceTable 

1289 self.YDeviceTable = YDeviceTable 

1290 

1291 

1292@VariationMerger.merger(otBase.ValueRecord) 

1293def merge(merger, self, lst): 

1294 for name, tableName in [ 

1295 ("XAdvance", "XAdvDevice"), 

1296 ("YAdvance", "YAdvDevice"), 

1297 ("XPlacement", "XPlaDevice"), 

1298 ("YPlacement", "YPlaDevice"), 

1299 ]: 

1300 if hasattr(self, name): 

1301 value, deviceTable = buildVarDevTable( 

1302 merger.store_builder, [getattr(a, name, 0) for a in lst] 

1303 ) 

1304 setattr(self, name, value) 

1305 if deviceTable: 

1306 setattr(self, tableName, deviceTable) 

1307 

1308 

1309class COLRVariationMerger(VariationMerger): 

1310 """A specialized VariationMerger that takes multiple master fonts containing 

1311 COLRv1 tables, and builds a variable COLR font. 

1312 

1313 COLR tables are special in that variable subtables can be associated with 

1314 multiple delta-set indices (via VarIndexBase). 

1315 They also contain tables that must change their type (not simply the Format) 

1316 as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes 

1317 care of that too. 

1318 """ 

1319 

1320 def __init__(self, model, axisTags, font, allowLayerReuse=True): 

1321 VariationMerger.__init__(self, model, axisTags, font) 

1322 # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase 

1323 # between variable tables with same varIdxes. 

1324 self.varIndexCache = {} 

1325 # flat list of all the varIdxes generated while merging 

1326 self.varIdxes = [] 

1327 # set of id()s of the subtables that contain variations after merging 

1328 # and need to be upgraded to the associated VarType. 

1329 self.varTableIds = set() 

1330 # we keep these around for rebuilding a LayerList while merging PaintColrLayers 

1331 self.layers = [] 

1332 self.layerReuseCache = None 

1333 if allowLayerReuse: 

1334 self.layerReuseCache = LayerReuseCache() 

1335 # flag to ensure BaseGlyphList is fully merged before LayerList gets processed 

1336 self._doneBaseGlyphs = False 

1337 

1338 def mergeTables(self, font, master_ttfs, tableTags=("COLR",)): 

1339 if "COLR" in tableTags and "COLR" in font: 

1340 # The merger modifies the destination COLR table in-place. If this contains 

1341 # multiple PaintColrLayers referencing the same layers from LayerList, it's 

1342 # a problem because we may risk modifying the same paint more than once, or 

1343 # worse, fail while attempting to do that. 

1344 # We don't know whether the master COLR table was built with layer reuse 

1345 # disabled, thus to be safe we rebuild its LayerList so that it contains only 

1346 # unique layers referenced from non-overlapping PaintColrLayers throughout 

1347 # the base paint graphs. 

1348 self.expandPaintColrLayers(font["COLR"].table) 

1349 VariationMerger.mergeTables(self, font, master_ttfs, tableTags) 

1350 

1351 def checkFormatEnum(self, out, lst, validate=lambda _: True): 

1352 fmt = out.Format 

1353 formatEnum = out.formatEnum 

1354 ok = False 

1355 try: 

1356 fmt = formatEnum(fmt) 

1357 except ValueError: 

1358 pass 

1359 else: 

1360 ok = validate(fmt) 

1361 if not ok: 

1362 raise UnsupportedFormat(self, subtable=type(out).__name__, value=fmt) 

1363 expected = fmt 

1364 got = [] 

1365 for v in lst: 

1366 fmt = getattr(v, "Format", None) 

1367 try: 

1368 fmt = formatEnum(fmt) 

1369 except ValueError: 

1370 pass 

1371 got.append(fmt) 

1372 if not allEqualTo(expected, got): 

1373 raise InconsistentFormats( 

1374 self, 

1375 subtable=type(out).__name__, 

1376 expected=expected, 

1377 got=got, 

1378 ) 

1379 return expected 

1380 

1381 def mergeSparseDict(self, out, lst): 

1382 for k in out.keys(): 

1383 try: 

1384 self.mergeThings(out[k], [v.get(k) for v in lst]) 

1385 except VarLibMergeError as e: 

1386 e.stack.append(f"[{k!r}]") 

1387 raise 

1388 

1389 def mergeAttrs(self, out, lst, attrs): 

1390 for attr in attrs: 

1391 value = getattr(out, attr) 

1392 values = [getattr(item, attr) for item in lst] 

1393 try: 

1394 self.mergeThings(value, values) 

1395 except VarLibMergeError as e: 

1396 e.stack.append(f".{attr}") 

1397 raise 

1398 

1399 def storeMastersForAttr(self, out, lst, attr): 

1400 master_values = [getattr(item, attr) for item in lst] 

1401 

1402 # VarStore treats deltas for fixed-size floats as integers, so we 

1403 # must convert master values to int before storing them in the builder 

1404 # then back to float. 

1405 is_fixed_size_float = False 

1406 conv = out.getConverterByName(attr) 

1407 if isinstance(conv, BaseFixedValue): 

1408 is_fixed_size_float = True 

1409 master_values = [conv.toInt(v) for v in master_values] 

1410 

1411 baseValue = master_values[0] 

1412 varIdx = ot.NO_VARIATION_INDEX 

1413 if not allEqual(master_values): 

1414 baseValue, varIdx = self.store_builder.storeMasters(master_values) 

1415 

1416 if is_fixed_size_float: 

1417 baseValue = conv.fromInt(baseValue) 

1418 

1419 return baseValue, varIdx 

1420 

1421 def storeVariationIndices(self, varIdxes) -> int: 

1422 # try to reuse an existing VarIndexBase for the same varIdxes, or else 

1423 # create a new one 

1424 key = tuple(varIdxes) 

1425 varIndexBase = self.varIndexCache.get(key) 

1426 

1427 if varIndexBase is None: 

1428 # scan for a full match anywhere in the self.varIdxes 

1429 for i in range(len(self.varIdxes) - len(varIdxes) + 1): 

1430 if self.varIdxes[i : i + len(varIdxes)] == varIdxes: 

1431 self.varIndexCache[key] = varIndexBase = i 

1432 break 

1433 

1434 if varIndexBase is None: 

1435 # try find a partial match at the end of the self.varIdxes 

1436 for n in range(len(varIdxes) - 1, 0, -1): 

1437 if self.varIdxes[-n:] == varIdxes[:n]: 

1438 varIndexBase = len(self.varIdxes) - n 

1439 self.varIndexCache[key] = varIndexBase 

1440 self.varIdxes.extend(varIdxes[n:]) 

1441 break 

1442 

1443 if varIndexBase is None: 

1444 # no match found, append at the end 

1445 self.varIndexCache[key] = varIndexBase = len(self.varIdxes) 

1446 self.varIdxes.extend(varIdxes) 

1447 

1448 return varIndexBase 

1449 

1450 def mergeVariableAttrs(self, out, lst, attrs) -> int: 

1451 varIndexBase = ot.NO_VARIATION_INDEX 

1452 varIdxes = [] 

1453 for attr in attrs: 

1454 baseValue, varIdx = self.storeMastersForAttr(out, lst, attr) 

1455 setattr(out, attr, baseValue) 

1456 varIdxes.append(varIdx) 

1457 

1458 if any(v != ot.NO_VARIATION_INDEX for v in varIdxes): 

1459 varIndexBase = self.storeVariationIndices(varIdxes) 

1460 

1461 return varIndexBase 

1462 

1463 @classmethod 

1464 def convertSubTablesToVarType(cls, table): 

1465 for path in dfs_base_table( 

1466 table, 

1467 skip_root=True, 

1468 predicate=lambda path: ( 

1469 getattr(type(path[-1].value), "VarType", None) is not None 

1470 ), 

1471 ): 

1472 st = path[-1] 

1473 subTable = st.value 

1474 varType = type(subTable).VarType 

1475 newSubTable = varType() 

1476 newSubTable.__dict__.update(subTable.__dict__) 

1477 newSubTable.populateDefaults() 

1478 parent = path[-2].value 

1479 if st.index is not None: 

1480 getattr(parent, st.name)[st.index] = newSubTable 

1481 else: 

1482 setattr(parent, st.name, newSubTable) 

1483 

1484 @staticmethod 

1485 def expandPaintColrLayers(colr): 

1486 """Rebuild LayerList without PaintColrLayers reuse. 

1487 

1488 Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph 

1489 which are irrelevant for this); any layers referenced via PaintColrLayers are 

1490 collected into a new LayerList and duplicated when reuse is detected, to ensure 

1491 that all paints are distinct objects at the end of the process. 

1492 PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap 

1493 is left. Also, any consecutively nested PaintColrLayers are flattened. 

1494 The COLR table's LayerList is replaced with the new unique layers. 

1495 A side effect is also that any layer from the old LayerList which is not 

1496 referenced by any PaintColrLayers is dropped. 

1497 """ 

1498 if not colr.LayerList: 

1499 # if no LayerList, there's nothing to expand 

1500 return 

1501 uniqueLayerIDs = set() 

1502 newLayerList = [] 

1503 for rec in colr.BaseGlyphList.BaseGlyphPaintRecord: 

1504 frontier = [rec.Paint] 

1505 while frontier: 

1506 paint = frontier.pop() 

1507 if paint.Format == ot.PaintFormat.PaintColrGlyph: 

1508 # don't traverse these, we treat them as constant for merging 

1509 continue 

1510 elif paint.Format == ot.PaintFormat.PaintColrLayers: 

1511 # de-treeify any nested PaintColrLayers, append unique copies to 

1512 # the new layer list and update PaintColrLayers index/count 

1513 children = list(_flatten_layers(paint, colr)) 

1514 first_layer_index = len(newLayerList) 

1515 for layer in children: 

1516 if id(layer) in uniqueLayerIDs: 

1517 layer = copy.deepcopy(layer) 

1518 assert id(layer) not in uniqueLayerIDs 

1519 newLayerList.append(layer) 

1520 uniqueLayerIDs.add(id(layer)) 

1521 paint.FirstLayerIndex = first_layer_index 

1522 paint.NumLayers = len(children) 

1523 else: 

1524 children = paint.getChildren(colr) 

1525 frontier.extend(reversed(children)) 

1526 # sanity check all the new layers are distinct objects 

1527 assert len(newLayerList) == len(uniqueLayerIDs) 

1528 colr.LayerList.Paint = newLayerList 

1529 colr.LayerList.LayerCount = len(newLayerList) 

1530 

1531 

1532@COLRVariationMerger.merger(ot.BaseGlyphList) 

1533def merge(merger, self, lst): 

1534 # ignore BaseGlyphCount, allow sparse glyph sets across masters 

1535 out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord} 

1536 masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst] 

1537 

1538 for i, g in enumerate(out.keys()): 

1539 try: 

1540 # missing base glyphs don't participate in the merge 

1541 merger.mergeThings(out[g], [v.get(g) for v in masters]) 

1542 except VarLibMergeError as e: 

1543 e.stack.append(f".BaseGlyphPaintRecord[{i}]") 

1544 e.cause["location"] = f"base glyph {g!r}" 

1545 raise 

1546 

1547 merger._doneBaseGlyphs = True 

1548 

1549 

1550@COLRVariationMerger.merger(ot.LayerList) 

1551def merge(merger, self, lst): 

1552 # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers 

1553 # found while traversing the paint graphs rooted at BaseGlyphPaintRecords. 

1554 assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList" 

1555 # Simply flush the final list of layers and go home. 

1556 self.LayerCount = len(merger.layers) 

1557 self.Paint = merger.layers 

1558 

1559 

1560def _flatten_layers(root, colr): 

1561 assert root.Format == ot.PaintFormat.PaintColrLayers 

1562 for paint in root.getChildren(colr): 

1563 if paint.Format == ot.PaintFormat.PaintColrLayers: 

1564 yield from _flatten_layers(paint, colr) 

1565 else: 

1566 yield paint 

1567 

1568 

1569def _merge_PaintColrLayers(self, out, lst): 

1570 # we only enforce that the (flat) number of layers is the same across all masters 

1571 # but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets. 

1572 

1573 out_layers = list(_flatten_layers(out, self.font["COLR"].table)) 

1574 

1575 # sanity check ttfs are subset to current values (see VariationMerger.mergeThings) 

1576 # before matching each master PaintColrLayers to its respective COLR by position 

1577 assert len(self.ttfs) == len(lst) 

1578 master_layerses = [ 

1579 list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table)) 

1580 for i in range(len(lst)) 

1581 ] 

1582 

1583 try: 

1584 self.mergeLists(out_layers, master_layerses) 

1585 except VarLibMergeError as e: 

1586 # NOTE: This attribute doesn't actually exist in PaintColrLayers but it's 

1587 # handy to have it in the stack trace for debugging. 

1588 e.stack.append(".Layers") 

1589 raise 

1590 

1591 # following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers 

1592 # but I couldn't find a nice way to share the code between the two... 

1593 

1594 if self.layerReuseCache is not None: 

1595 # successful reuse can make the list smaller 

1596 out_layers = self.layerReuseCache.try_reuse(out_layers) 

1597 

1598 # if the list is still too big we need to tree-fy it 

1599 is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT 

1600 out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT) 

1601 

1602 # We now have a tree of sequences with Paint leaves. 

1603 # Convert the sequences into PaintColrLayers. 

1604 def listToColrLayers(paint): 

1605 if isinstance(paint, list): 

1606 layers = [listToColrLayers(l) for l in paint] 

1607 paint = ot.Paint() 

1608 paint.Format = int(ot.PaintFormat.PaintColrLayers) 

1609 paint.NumLayers = len(layers) 

1610 paint.FirstLayerIndex = len(self.layers) 

1611 self.layers.extend(layers) 

1612 if self.layerReuseCache is not None: 

1613 self.layerReuseCache.add(layers, paint.FirstLayerIndex) 

1614 return paint 

1615 

1616 out_layers = [listToColrLayers(l) for l in out_layers] 

1617 

1618 if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers: 

1619 # special case when the reuse cache finds a single perfect PaintColrLayers match 

1620 # (it can only come from a successful reuse, _flatten_layers has gotten rid of 

1621 # all nested PaintColrLayers already); we assign it directly and avoid creating 

1622 # an extra table 

1623 out.NumLayers = out_layers[0].NumLayers 

1624 out.FirstLayerIndex = out_layers[0].FirstLayerIndex 

1625 else: 

1626 out.NumLayers = len(out_layers) 

1627 out.FirstLayerIndex = len(self.layers) 

1628 

1629 self.layers.extend(out_layers) 

1630 

1631 # Register our parts for reuse provided we aren't a tree 

1632 # If we are a tree the leaves registered for reuse and that will suffice 

1633 if self.layerReuseCache is not None and not is_tree: 

1634 self.layerReuseCache.add(out_layers, out.FirstLayerIndex) 

1635 

1636 

1637@COLRVariationMerger.merger((ot.Paint, ot.ClipBox)) 

1638def merge(merger, self, lst): 

1639 fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable()) 

1640 

1641 if fmt is ot.PaintFormat.PaintColrLayers: 

1642 _merge_PaintColrLayers(merger, self, lst) 

1643 return 

1644 

1645 varFormat = fmt.as_variable() 

1646 

1647 varAttrs = () 

1648 if varFormat is not None: 

1649 varAttrs = otBase.getVariableAttrs(type(self), varFormat) 

1650 staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs) 

1651 

1652 merger.mergeAttrs(self, lst, staticAttrs) 

1653 

1654 varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) 

1655 

1656 subTables = [st.value for st in self.iterSubTables()] 

1657 

1658 # Convert table to variable if itself has variations or any subtables have 

1659 isVariable = varIndexBase != ot.NO_VARIATION_INDEX or any( 

1660 id(table) in merger.varTableIds for table in subTables 

1661 ) 

1662 

1663 if isVariable: 

1664 if varAttrs: 

1665 # Some PaintVar* don't have any scalar attributes that can vary, 

1666 # only indirect offsets to other variable subtables, thus have 

1667 # no VarIndexBase of their own (e.g. PaintVarTransform) 

1668 self.VarIndexBase = varIndexBase 

1669 

1670 if subTables: 

1671 # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc. 

1672 merger.convertSubTablesToVarType(self) 

1673 

1674 assert varFormat is not None 

1675 self.Format = int(varFormat) 

1676 

1677 

1678@COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop)) 

1679def merge(merger, self, lst): 

1680 varType = type(self).VarType 

1681 

1682 varAttrs = otBase.getVariableAttrs(varType) 

1683 staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs) 

1684 

1685 merger.mergeAttrs(self, lst, staticAttrs) 

1686 

1687 varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) 

1688 

1689 if varIndexBase != ot.NO_VARIATION_INDEX: 

1690 self.VarIndexBase = varIndexBase 

1691 # mark as having variations so the parent table will convert to Var{Type} 

1692 merger.varTableIds.add(id(self)) 

1693 

1694 

1695@COLRVariationMerger.merger(ot.ColorLine) 

1696def merge(merger, self, lst): 

1697 merger.mergeAttrs(self, lst, (c.name for c in self.getConverters())) 

1698 

1699 if any(id(stop) in merger.varTableIds for stop in self.ColorStop): 

1700 merger.convertSubTablesToVarType(self) 

1701 merger.varTableIds.add(id(self)) 

1702 

1703 

1704@COLRVariationMerger.merger(ot.ClipList, "clips") 

1705def merge(merger, self, lst): 

1706 # 'sparse' in that we allow non-default masters to omit ClipBox entries 

1707 # for some/all glyphs (i.e. they don't participate) 

1708 merger.mergeSparseDict(self, lst)