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

918 statements  

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

1from fontTools.config import OPTIONS 

2from fontTools.misc.textTools import Tag, bytesjoin 

3from .DefaultTable import DefaultTable 

4from enum import IntEnum 

5import sys 

6import array 

7import struct 

8import logging 

9from functools import lru_cache 

10from typing import Iterator, NamedTuple, Optional, Tuple 

11 

12log = logging.getLogger(__name__) 

13 

14have_uharfbuzz = False 

15try: 

16 import uharfbuzz as hb 

17 

18 # repack method added in uharfbuzz >= 0.23; if uharfbuzz *can* be 

19 # imported but repack method is missing, behave as if uharfbuzz 

20 # is not available (fallback to the slower Python implementation) 

21 have_uharfbuzz = callable(getattr(hb, "repack", None)) 

22except ImportError: 

23 pass 

24 

25USE_HARFBUZZ_REPACKER = OPTIONS[f"{__name__}:USE_HARFBUZZ_REPACKER"] 

26 

27 

28class OverflowErrorRecord(object): 

29 def __init__(self, overflowTuple): 

30 self.tableType = overflowTuple[0] 

31 self.LookupListIndex = overflowTuple[1] 

32 self.SubTableIndex = overflowTuple[2] 

33 self.itemName = overflowTuple[3] 

34 self.itemIndex = overflowTuple[4] 

35 

36 def __repr__(self): 

37 return str( 

38 ( 

39 self.tableType, 

40 "LookupIndex:", 

41 self.LookupListIndex, 

42 "SubTableIndex:", 

43 self.SubTableIndex, 

44 "ItemName:", 

45 self.itemName, 

46 "ItemIndex:", 

47 self.itemIndex, 

48 ) 

49 ) 

50 

51 

52class OTLOffsetOverflowError(Exception): 

53 def __init__(self, overflowErrorRecord): 

54 self.value = overflowErrorRecord 

55 

56 def __str__(self): 

57 return repr(self.value) 

58 

59 

60class RepackerState(IntEnum): 

61 # Repacking control flow is implemnted using a state machine. The state machine table: 

62 # 

63 # State | Packing Success | Packing Failed | Exception Raised | 

64 # ------------+-----------------+----------------+------------------+ 

65 # PURE_FT | Return result | PURE_FT | Return failure | 

66 # HB_FT | Return result | HB_FT | FT_FALLBACK | 

67 # FT_FALLBACK | HB_FT | FT_FALLBACK | Return failure | 

68 

69 # Pack only with fontTools, don't allow sharing between extensions. 

70 PURE_FT = 1 

71 

72 # Attempt to pack with harfbuzz (allowing sharing between extensions) 

73 # use fontTools to attempt overflow resolution. 

74 HB_FT = 2 

75 

76 # Fallback if HB/FT packing gets stuck. Pack only with fontTools, don't allow sharing between 

77 # extensions. 

78 FT_FALLBACK = 3 

79 

80 

81class BaseTTXConverter(DefaultTable): 

82 

83 """Generic base class for TTX table converters. It functions as an 

84 adapter between the TTX (ttLib actually) table model and the model 

85 we use for OpenType tables, which is necessarily subtly different. 

86 """ 

87 

88 def decompile(self, data, font): 

89 """Create an object from the binary data. Called automatically on access.""" 

90 from . import otTables 

91 

92 reader = OTTableReader(data, tableTag=self.tableTag) 

93 tableClass = getattr(otTables, self.tableTag) 

94 self.table = tableClass() 

95 self.table.decompile(reader, font) 

96 

97 def compile(self, font): 

98 """Compiles the table into binary. Called automatically on save.""" 

99 

100 # General outline: 

101 # Create a top-level OTTableWriter for the GPOS/GSUB table. 

102 # Call the compile method for the the table 

103 # for each 'converter' record in the table converter list 

104 # call converter's write method for each item in the value. 

105 # - For simple items, the write method adds a string to the 

106 # writer's self.items list. 

107 # - For Struct/Table/Subtable items, it add first adds new writer to the 

108 # to the writer's self.items, then calls the item's compile method. 

109 # This creates a tree of writers, rooted at the GUSB/GPOS writer, with 

110 # each writer representing a table, and the writer.items list containing 

111 # the child data strings and writers. 

112 # call the getAllData method 

113 # call _doneWriting, which removes duplicates 

114 # call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables 

115 # Traverse the flat list of tables, calling getDataLength on each to update their position 

116 # Traverse the flat list of tables again, calling getData each get the data in the table, now that 

117 # pos's and offset are known. 

118 

119 # If a lookup subtable overflows an offset, we have to start all over. 

120 overflowRecord = None 

121 # this is 3-state option: default (None) means automatically use hb.repack or 

122 # silently fall back if it fails; True, use it and raise error if not possible 

123 # or it errors out; False, don't use it, even if you can. 

124 use_hb_repack = font.cfg[USE_HARFBUZZ_REPACKER] 

125 if self.tableTag in ("GSUB", "GPOS"): 

126 if use_hb_repack is False: 

127 log.debug( 

128 "hb.repack disabled, compiling '%s' with pure-python serializer", 

129 self.tableTag, 

130 ) 

131 elif not have_uharfbuzz: 

132 if use_hb_repack is True: 

133 raise ImportError("No module named 'uharfbuzz'") 

134 else: 

135 assert use_hb_repack is None 

136 log.debug( 

137 "uharfbuzz not found, compiling '%s' with pure-python serializer", 

138 self.tableTag, 

139 ) 

140 

141 if ( 

142 use_hb_repack in (None, True) 

143 and have_uharfbuzz 

144 and self.tableTag in ("GSUB", "GPOS") 

145 ): 

146 state = RepackerState.HB_FT 

147 else: 

148 state = RepackerState.PURE_FT 

149 

150 hb_first_error_logged = False 

151 lastOverflowRecord = None 

152 while True: 

153 try: 

154 writer = OTTableWriter(tableTag=self.tableTag) 

155 self.table.compile(writer, font) 

156 if state == RepackerState.HB_FT: 

157 return self.tryPackingHarfbuzz(writer, hb_first_error_logged) 

158 elif state == RepackerState.PURE_FT: 

159 return self.tryPackingFontTools(writer) 

160 elif state == RepackerState.FT_FALLBACK: 

161 # Run packing with FontTools only, but don't return the result as it will 

162 # not be optimally packed. Once a successful packing has been found, state is 

163 # changed back to harfbuzz packing to produce the final, optimal, packing. 

164 self.tryPackingFontTools(writer) 

165 log.debug( 

166 "Re-enabling sharing between extensions and switching back to " 

167 "harfbuzz+fontTools packing." 

168 ) 

169 state = RepackerState.HB_FT 

170 

171 except OTLOffsetOverflowError as e: 

172 hb_first_error_logged = True 

173 ok = self.tryResolveOverflow(font, e, lastOverflowRecord) 

174 lastOverflowRecord = e.value 

175 

176 if ok: 

177 continue 

178 

179 if state is RepackerState.HB_FT: 

180 log.debug( 

181 "Harfbuzz packing out of resolutions, disabling sharing between extensions and " 

182 "switching to fontTools only packing." 

183 ) 

184 state = RepackerState.FT_FALLBACK 

185 else: 

186 raise 

187 

188 def tryPackingHarfbuzz(self, writer, hb_first_error_logged): 

189 try: 

190 log.debug("serializing '%s' with hb.repack", self.tableTag) 

191 return writer.getAllDataUsingHarfbuzz(self.tableTag) 

192 except (ValueError, MemoryError, hb.RepackerError) as e: 

193 # Only log hb repacker errors the first time they occur in 

194 # the offset-overflow resolution loop, they are just noisy. 

195 # Maybe we can revisit this if/when uharfbuzz actually gives 

196 # us more info as to why hb.repack failed... 

197 if not hb_first_error_logged: 

198 error_msg = f"{type(e).__name__}" 

199 if str(e) != "": 

200 error_msg += f": {e}" 

201 log.warning( 

202 "hb.repack failed to serialize '%s', attempting fonttools resolutions " 

203 "; the error message was: %s", 

204 self.tableTag, 

205 error_msg, 

206 ) 

207 hb_first_error_logged = True 

208 return writer.getAllData(remove_duplicate=False) 

209 

210 def tryPackingFontTools(self, writer): 

211 return writer.getAllData() 

212 

213 def tryResolveOverflow(self, font, e, lastOverflowRecord): 

214 ok = 0 

215 if lastOverflowRecord == e.value: 

216 # Oh well... 

217 return ok 

218 

219 overflowRecord = e.value 

220 log.info("Attempting to fix OTLOffsetOverflowError %s", e) 

221 

222 if overflowRecord.itemName is None: 

223 from .otTables import fixLookupOverFlows 

224 

225 ok = fixLookupOverFlows(font, overflowRecord) 

226 else: 

227 from .otTables import fixSubTableOverFlows 

228 

229 ok = fixSubTableOverFlows(font, overflowRecord) 

230 

231 if ok: 

232 return ok 

233 

234 # Try upgrading lookup to Extension and hope 

235 # that cross-lookup sharing not happening would 

236 # fix overflow... 

237 from .otTables import fixLookupOverFlows 

238 

239 return fixLookupOverFlows(font, overflowRecord) 

240 

241 def toXML(self, writer, font): 

242 self.table.toXML2(writer, font) 

243 

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

245 from . import otTables 

246 

247 if not hasattr(self, "table"): 

248 tableClass = getattr(otTables, self.tableTag) 

249 self.table = tableClass() 

250 self.table.fromXML(name, attrs, content, font) 

251 self.table.populateDefaults() 

252 

253 def ensureDecompiled(self, recurse=True): 

254 self.table.ensureDecompiled(recurse=recurse) 

255 

256 

257# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928 

258assert len(struct.pack("i", 0)) == 4 

259assert array.array("i").itemsize == 4, "Oops, file a bug against fonttools." 

260 

261 

262class OTTableReader(object): 

263 

264 """Helper class to retrieve data from an OpenType table.""" 

265 

266 __slots__ = ("data", "offset", "pos", "localState", "tableTag") 

267 

268 def __init__(self, data, localState=None, offset=0, tableTag=None): 

269 self.data = data 

270 self.offset = offset 

271 self.pos = offset 

272 self.localState = localState 

273 self.tableTag = tableTag 

274 

275 def advance(self, count): 

276 self.pos += count 

277 

278 def seek(self, pos): 

279 self.pos = pos 

280 

281 def copy(self): 

282 other = self.__class__(self.data, self.localState, self.offset, self.tableTag) 

283 other.pos = self.pos 

284 return other 

285 

286 def getSubReader(self, offset): 

287 offset = self.offset + offset 

288 return self.__class__(self.data, self.localState, offset, self.tableTag) 

289 

290 def readValue(self, typecode, staticSize): 

291 pos = self.pos 

292 newpos = pos + staticSize 

293 (value,) = struct.unpack(f">{typecode}", self.data[pos:newpos]) 

294 self.pos = newpos 

295 return value 

296 

297 def readArray(self, typecode, staticSize, count): 

298 pos = self.pos 

299 newpos = pos + count * staticSize 

300 value = array.array(typecode, self.data[pos:newpos]) 

301 if sys.byteorder != "big": 

302 value.byteswap() 

303 self.pos = newpos 

304 return value.tolist() 

305 

306 def readInt8(self): 

307 return self.readValue("b", staticSize=1) 

308 

309 def readInt8Array(self, count): 

310 return self.readArray("b", staticSize=1, count=count) 

311 

312 def readShort(self): 

313 return self.readValue("h", staticSize=2) 

314 

315 def readShortArray(self, count): 

316 return self.readArray("h", staticSize=2, count=count) 

317 

318 def readLong(self): 

319 return self.readValue("i", staticSize=4) 

320 

321 def readLongArray(self, count): 

322 return self.readArray("i", staticSize=4, count=count) 

323 

324 def readUInt8(self): 

325 return self.readValue("B", staticSize=1) 

326 

327 def readUInt8Array(self, count): 

328 return self.readArray("B", staticSize=1, count=count) 

329 

330 def readUShort(self): 

331 return self.readValue("H", staticSize=2) 

332 

333 def readUShortArray(self, count): 

334 return self.readArray("H", staticSize=2, count=count) 

335 

336 def readULong(self): 

337 return self.readValue("I", staticSize=4) 

338 

339 def readULongArray(self, count): 

340 return self.readArray("I", staticSize=4, count=count) 

341 

342 def readUInt24(self): 

343 pos = self.pos 

344 newpos = pos + 3 

345 (value,) = struct.unpack(">l", b"\0" + self.data[pos:newpos]) 

346 self.pos = newpos 

347 return value 

348 

349 def readUInt24Array(self, count): 

350 return [self.readUInt24() for _ in range(count)] 

351 

352 def readTag(self): 

353 pos = self.pos 

354 newpos = pos + 4 

355 value = Tag(self.data[pos:newpos]) 

356 assert len(value) == 4, value 

357 self.pos = newpos 

358 return value 

359 

360 def readData(self, count): 

361 pos = self.pos 

362 newpos = pos + count 

363 value = self.data[pos:newpos] 

364 self.pos = newpos 

365 return value 

366 

367 def __setitem__(self, name, value): 

368 state = self.localState.copy() if self.localState else dict() 

369 state[name] = value 

370 self.localState = state 

371 

372 def __getitem__(self, name): 

373 return self.localState and self.localState[name] 

374 

375 def __contains__(self, name): 

376 return self.localState and name in self.localState 

377 

378 

379class OffsetToWriter(object): 

380 def __init__(self, subWriter, offsetSize): 

381 self.subWriter = subWriter 

382 self.offsetSize = offsetSize 

383 

384 def __eq__(self, other): 

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

386 return NotImplemented 

387 return self.subWriter == other.subWriter and self.offsetSize == other.offsetSize 

388 

389 def __hash__(self): 

390 # only works after self._doneWriting() has been called 

391 return hash((self.subWriter, self.offsetSize)) 

392 

393 

394class OTTableWriter(object): 

395 

396 """Helper class to gather and assemble data for OpenType tables.""" 

397 

398 def __init__(self, localState=None, tableTag=None): 

399 self.items = [] 

400 self.pos = None 

401 self.localState = localState 

402 self.tableTag = tableTag 

403 self.parent = None 

404 

405 def __setitem__(self, name, value): 

406 state = self.localState.copy() if self.localState else dict() 

407 state[name] = value 

408 self.localState = state 

409 

410 def __getitem__(self, name): 

411 return self.localState[name] 

412 

413 def __delitem__(self, name): 

414 del self.localState[name] 

415 

416 # assembler interface 

417 

418 def getDataLength(self): 

419 """Return the length of this table in bytes, without subtables.""" 

420 l = 0 

421 for item in self.items: 

422 if hasattr(item, "getCountData"): 

423 l += item.size 

424 elif hasattr(item, "subWriter"): 

425 l += item.offsetSize 

426 else: 

427 l = l + len(item) 

428 return l 

429 

430 def getData(self): 

431 """Assemble the data for this writer/table, without subtables.""" 

432 items = list(self.items) # make a shallow copy 

433 pos = self.pos 

434 numItems = len(items) 

435 for i in range(numItems): 

436 item = items[i] 

437 

438 if hasattr(item, "subWriter"): 

439 if item.offsetSize == 4: 

440 items[i] = packULong(item.subWriter.pos - pos) 

441 elif item.offsetSize == 2: 

442 try: 

443 items[i] = packUShort(item.subWriter.pos - pos) 

444 except struct.error: 

445 # provide data to fix overflow problem. 

446 overflowErrorRecord = self.getOverflowErrorRecord( 

447 item.subWriter 

448 ) 

449 

450 raise OTLOffsetOverflowError(overflowErrorRecord) 

451 elif item.offsetSize == 3: 

452 items[i] = packUInt24(item.subWriter.pos - pos) 

453 else: 

454 raise ValueError(item.offsetSize) 

455 

456 return bytesjoin(items) 

457 

458 def getDataForHarfbuzz(self): 

459 """Assemble the data for this writer/table with all offset field set to 0""" 

460 items = list(self.items) 

461 packFuncs = {2: packUShort, 3: packUInt24, 4: packULong} 

462 for i, item in enumerate(items): 

463 if hasattr(item, "subWriter"): 

464 # Offset value is not needed in harfbuzz repacker, so setting offset to 0 to avoid overflow here 

465 if item.offsetSize in packFuncs: 

466 items[i] = packFuncs[item.offsetSize](0) 

467 else: 

468 raise ValueError(item.offsetSize) 

469 

470 return bytesjoin(items) 

471 

472 def __hash__(self): 

473 # only works after self._doneWriting() has been called 

474 return hash(self.items) 

475 

476 def __ne__(self, other): 

477 result = self.__eq__(other) 

478 return result if result is NotImplemented else not result 

479 

480 def __eq__(self, other): 

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

482 return NotImplemented 

483 return self.items == other.items 

484 

485 def _doneWriting(self, internedTables, shareExtension=False): 

486 # Convert CountData references to data string items 

487 # collapse duplicate table references to a unique entry 

488 # "tables" are OTTableWriter objects. 

489 

490 # For Extension Lookup types, we can 

491 # eliminate duplicates only within the tree under the Extension Lookup, 

492 # as offsets may exceed 64K even between Extension LookupTable subtables. 

493 isExtension = hasattr(self, "Extension") 

494 

495 # Certain versions of Uniscribe reject the font if the GSUB/GPOS top-level 

496 # arrays (ScriptList, FeatureList, LookupList) point to the same, possibly 

497 # empty, array. So, we don't share those. 

498 # See: https://github.com/fonttools/fonttools/issues/518 

499 dontShare = hasattr(self, "DontShare") 

500 

501 if isExtension and not shareExtension: 

502 internedTables = {} 

503 

504 items = self.items 

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

506 item = items[i] 

507 if hasattr(item, "getCountData"): 

508 items[i] = item.getCountData() 

509 elif hasattr(item, "subWriter"): 

510 item.subWriter._doneWriting( 

511 internedTables, shareExtension=shareExtension 

512 ) 

513 # At this point, all subwriters are hashable based on their items. 

514 # (See hash and comparison magic methods above.) So the ``setdefault`` 

515 # call here will return the first writer object we've seen with 

516 # equal content, or store it in the dictionary if it's not been 

517 # seen yet. We therefore replace the subwriter object with an equivalent 

518 # object, which deduplicates the tree. 

519 if not dontShare: 

520 items[i].subWriter = internedTables.setdefault( 

521 item.subWriter, item.subWriter 

522 ) 

523 self.items = tuple(items) 

524 

525 def _gatherTables(self, tables, extTables, done): 

526 # Convert table references in self.items tree to a flat 

527 # list of tables in depth-first traversal order. 

528 # "tables" are OTTableWriter objects. 

529 # We do the traversal in reverse order at each level, in order to 

530 # resolve duplicate references to be the last reference in the list of tables. 

531 # For extension lookups, duplicate references can be merged only within the 

532 # writer tree under the extension lookup. 

533 

534 done[id(self)] = True 

535 

536 numItems = len(self.items) 

537 iRange = list(range(numItems)) 

538 iRange.reverse() 

539 

540 isExtension = hasattr(self, "Extension") 

541 

542 selfTables = tables 

543 

544 if isExtension: 

545 assert ( 

546 extTables is not None 

547 ), "Program or XML editing error. Extension subtables cannot contain extensions subtables" 

548 tables, extTables, done = extTables, None, {} 

549 

550 # add Coverage table if it is sorted last. 

551 sortCoverageLast = False 

552 if hasattr(self, "sortCoverageLast"): 

553 # Find coverage table 

554 for i in range(numItems): 

555 item = self.items[i] 

556 if ( 

557 hasattr(item, "subWriter") 

558 and getattr(item.subWriter, "name", None) == "Coverage" 

559 ): 

560 sortCoverageLast = True 

561 break 

562 if id(item.subWriter) not in done: 

563 item.subWriter._gatherTables(tables, extTables, done) 

564 else: 

565 # We're a new parent of item 

566 pass 

567 

568 for i in iRange: 

569 item = self.items[i] 

570 if not hasattr(item, "subWriter"): 

571 continue 

572 

573 if ( 

574 sortCoverageLast 

575 and (i == 1) 

576 and getattr(item.subWriter, "name", None) == "Coverage" 

577 ): 

578 # we've already 'gathered' it above 

579 continue 

580 

581 if id(item.subWriter) not in done: 

582 item.subWriter._gatherTables(tables, extTables, done) 

583 else: 

584 # Item is already written out by other parent 

585 pass 

586 

587 selfTables.append(self) 

588 

589 def _gatherGraphForHarfbuzz(self, tables, obj_list, done, objidx, virtual_edges): 

590 real_links = [] 

591 virtual_links = [] 

592 item_idx = objidx 

593 

594 # Merge virtual_links from parent 

595 for idx in virtual_edges: 

596 virtual_links.append((0, 0, idx)) 

597 

598 sortCoverageLast = False 

599 coverage_idx = 0 

600 if hasattr(self, "sortCoverageLast"): 

601 # Find coverage table 

602 for i, item in enumerate(self.items): 

603 if getattr(item, "name", None) == "Coverage": 

604 sortCoverageLast = True 

605 if id(item) not in done: 

606 coverage_idx = item_idx = item._gatherGraphForHarfbuzz( 

607 tables, obj_list, done, item_idx, virtual_edges 

608 ) 

609 else: 

610 coverage_idx = done[id(item)] 

611 virtual_edges.append(coverage_idx) 

612 break 

613 

614 child_idx = 0 

615 offset_pos = 0 

616 for i, item in enumerate(self.items): 

617 if hasattr(item, "subWriter"): 

618 pos = offset_pos 

619 elif hasattr(item, "getCountData"): 

620 offset_pos += item.size 

621 continue 

622 else: 

623 offset_pos = offset_pos + len(item) 

624 continue 

625 

626 if id(item.subWriter) not in done: 

627 child_idx = item_idx = item.subWriter._gatherGraphForHarfbuzz( 

628 tables, obj_list, done, item_idx, virtual_edges 

629 ) 

630 else: 

631 child_idx = done[id(item.subWriter)] 

632 

633 real_edge = (pos, item.offsetSize, child_idx) 

634 real_links.append(real_edge) 

635 offset_pos += item.offsetSize 

636 

637 tables.append(self) 

638 obj_list.append((real_links, virtual_links)) 

639 item_idx += 1 

640 done[id(self)] = item_idx 

641 if sortCoverageLast: 

642 virtual_edges.pop() 

643 

644 return item_idx 

645 

646 def getAllDataUsingHarfbuzz(self, tableTag): 

647 """The Whole table is represented as a Graph. 

648 Assemble graph data and call Harfbuzz repacker to pack the table. 

649 Harfbuzz repacker is faster and retain as much sub-table sharing as possible, see also: 

650 https://github.com/harfbuzz/harfbuzz/blob/main/docs/repacker.md 

651 The input format for hb.repack() method is explained here: 

652 https://github.com/harfbuzz/uharfbuzz/blob/main/src/uharfbuzz/_harfbuzz.pyx#L1149 

653 """ 

654 internedTables = {} 

655 self._doneWriting(internedTables, shareExtension=True) 

656 tables = [] 

657 obj_list = [] 

658 done = {} 

659 objidx = 0 

660 virtual_edges = [] 

661 self._gatherGraphForHarfbuzz(tables, obj_list, done, objidx, virtual_edges) 

662 # Gather all data in two passes: the absolute positions of all 

663 # subtable are needed before the actual data can be assembled. 

664 pos = 0 

665 for table in tables: 

666 table.pos = pos 

667 pos = pos + table.getDataLength() 

668 

669 data = [] 

670 for table in tables: 

671 tableData = table.getDataForHarfbuzz() 

672 data.append(tableData) 

673 

674 if hasattr(hb, "repack_with_tag"): 

675 return hb.repack_with_tag(str(tableTag), data, obj_list) 

676 else: 

677 return hb.repack(data, obj_list) 

678 

679 def getAllData(self, remove_duplicate=True): 

680 """Assemble all data, including all subtables.""" 

681 if remove_duplicate: 

682 internedTables = {} 

683 self._doneWriting(internedTables) 

684 tables = [] 

685 extTables = [] 

686 done = {} 

687 self._gatherTables(tables, extTables, done) 

688 tables.reverse() 

689 extTables.reverse() 

690 # Gather all data in two passes: the absolute positions of all 

691 # subtable are needed before the actual data can be assembled. 

692 pos = 0 

693 for table in tables: 

694 table.pos = pos 

695 pos = pos + table.getDataLength() 

696 

697 for table in extTables: 

698 table.pos = pos 

699 pos = pos + table.getDataLength() 

700 

701 data = [] 

702 for table in tables: 

703 tableData = table.getData() 

704 data.append(tableData) 

705 

706 for table in extTables: 

707 tableData = table.getData() 

708 data.append(tableData) 

709 

710 return bytesjoin(data) 

711 

712 # interface for gathering data, as used by table.compile() 

713 

714 def getSubWriter(self): 

715 subwriter = self.__class__(self.localState, self.tableTag) 

716 subwriter.parent = ( 

717 self # because some subtables have idential values, we discard 

718 ) 

719 # the duplicates under the getAllData method. Hence some 

720 # subtable writers can have more than one parent writer. 

721 # But we just care about first one right now. 

722 return subwriter 

723 

724 def writeValue(self, typecode, value): 

725 self.items.append(struct.pack(f">{typecode}", value)) 

726 

727 def writeArray(self, typecode, values): 

728 a = array.array(typecode, values) 

729 if sys.byteorder != "big": 

730 a.byteswap() 

731 self.items.append(a.tobytes()) 

732 

733 def writeInt8(self, value): 

734 assert -128 <= value < 128, value 

735 self.items.append(struct.pack(">b", value)) 

736 

737 def writeInt8Array(self, values): 

738 self.writeArray("b", values) 

739 

740 def writeShort(self, value): 

741 assert -32768 <= value < 32768, value 

742 self.items.append(struct.pack(">h", value)) 

743 

744 def writeShortArray(self, values): 

745 self.writeArray("h", values) 

746 

747 def writeLong(self, value): 

748 self.items.append(struct.pack(">i", value)) 

749 

750 def writeLongArray(self, values): 

751 self.writeArray("i", values) 

752 

753 def writeUInt8(self, value): 

754 assert 0 <= value < 256, value 

755 self.items.append(struct.pack(">B", value)) 

756 

757 def writeUInt8Array(self, values): 

758 self.writeArray("B", values) 

759 

760 def writeUShort(self, value): 

761 assert 0 <= value < 0x10000, value 

762 self.items.append(struct.pack(">H", value)) 

763 

764 def writeUShortArray(self, values): 

765 self.writeArray("H", values) 

766 

767 def writeULong(self, value): 

768 self.items.append(struct.pack(">I", value)) 

769 

770 def writeULongArray(self, values): 

771 self.writeArray("I", values) 

772 

773 def writeUInt24(self, value): 

774 assert 0 <= value < 0x1000000, value 

775 b = struct.pack(">L", value) 

776 self.items.append(b[1:]) 

777 

778 def writeUInt24Array(self, values): 

779 for value in values: 

780 self.writeUInt24(value) 

781 

782 def writeTag(self, tag): 

783 tag = Tag(tag).tobytes() 

784 assert len(tag) == 4, tag 

785 self.items.append(tag) 

786 

787 def writeSubTable(self, subWriter, offsetSize): 

788 self.items.append(OffsetToWriter(subWriter, offsetSize)) 

789 

790 def writeCountReference(self, table, name, size=2, value=None): 

791 ref = CountReference(table, name, size=size, value=value) 

792 self.items.append(ref) 

793 return ref 

794 

795 def writeStruct(self, format, values): 

796 data = struct.pack(*(format,) + values) 

797 self.items.append(data) 

798 

799 def writeData(self, data): 

800 self.items.append(data) 

801 

802 def getOverflowErrorRecord(self, item): 

803 LookupListIndex = SubTableIndex = itemName = itemIndex = None 

804 if self.name == "LookupList": 

805 LookupListIndex = item.repeatIndex 

806 elif self.name == "Lookup": 

807 LookupListIndex = self.repeatIndex 

808 SubTableIndex = item.repeatIndex 

809 else: 

810 itemName = getattr(item, "name", "<none>") 

811 if hasattr(item, "repeatIndex"): 

812 itemIndex = item.repeatIndex 

813 if self.name == "SubTable": 

814 LookupListIndex = self.parent.repeatIndex 

815 SubTableIndex = self.repeatIndex 

816 elif self.name == "ExtSubTable": 

817 LookupListIndex = self.parent.parent.repeatIndex 

818 SubTableIndex = self.parent.repeatIndex 

819 else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable. 

820 itemName = ".".join([self.name, itemName]) 

821 p1 = self.parent 

822 while p1 and p1.name not in ["ExtSubTable", "SubTable"]: 

823 itemName = ".".join([p1.name, itemName]) 

824 p1 = p1.parent 

825 if p1: 

826 if p1.name == "ExtSubTable": 

827 LookupListIndex = p1.parent.parent.repeatIndex 

828 SubTableIndex = p1.parent.repeatIndex 

829 else: 

830 LookupListIndex = p1.parent.repeatIndex 

831 SubTableIndex = p1.repeatIndex 

832 

833 return OverflowErrorRecord( 

834 (self.tableTag, LookupListIndex, SubTableIndex, itemName, itemIndex) 

835 ) 

836 

837 

838class CountReference(object): 

839 """A reference to a Count value, not a count of references.""" 

840 

841 def __init__(self, table, name, size=None, value=None): 

842 self.table = table 

843 self.name = name 

844 self.size = size 

845 if value is not None: 

846 self.setValue(value) 

847 

848 def setValue(self, value): 

849 table = self.table 

850 name = self.name 

851 if table[name] is None: 

852 table[name] = value 

853 else: 

854 assert table[name] == value, (name, table[name], value) 

855 

856 def getValue(self): 

857 return self.table[self.name] 

858 

859 def getCountData(self): 

860 v = self.table[self.name] 

861 if v is None: 

862 v = 0 

863 return {1: packUInt8, 2: packUShort, 4: packULong}[self.size](v) 

864 

865 

866def packUInt8(value): 

867 return struct.pack(">B", value) 

868 

869 

870def packUShort(value): 

871 return struct.pack(">H", value) 

872 

873 

874def packULong(value): 

875 assert 0 <= value < 0x100000000, value 

876 return struct.pack(">I", value) 

877 

878 

879def packUInt24(value): 

880 assert 0 <= value < 0x1000000, value 

881 return struct.pack(">I", value)[1:] 

882 

883 

884class BaseTable(object): 

885 

886 """Generic base class for all OpenType (sub)tables.""" 

887 

888 def __getattr__(self, attr): 

889 reader = self.__dict__.get("reader") 

890 if reader: 

891 del self.reader 

892 font = self.font 

893 del self.font 

894 self.decompile(reader, font) 

895 return getattr(self, attr) 

896 

897 raise AttributeError(attr) 

898 

899 def ensureDecompiled(self, recurse=False): 

900 reader = self.__dict__.get("reader") 

901 if reader: 

902 del self.reader 

903 font = self.font 

904 del self.font 

905 self.decompile(reader, font) 

906 if recurse: 

907 for subtable in self.iterSubTables(): 

908 subtable.value.ensureDecompiled(recurse) 

909 

910 def __getstate__(self): 

911 # before copying/pickling 'lazy' objects, make a shallow copy of OTTableReader 

912 # https://github.com/fonttools/fonttools/issues/2965 

913 if "reader" in self.__dict__: 

914 state = self.__dict__.copy() 

915 state["reader"] = self.__dict__["reader"].copy() 

916 return state 

917 return self.__dict__ 

918 

919 @classmethod 

920 def getRecordSize(cls, reader): 

921 totalSize = 0 

922 for conv in cls.converters: 

923 size = conv.getRecordSize(reader) 

924 if size is NotImplemented: 

925 return NotImplemented 

926 countValue = 1 

927 if conv.repeat: 

928 if conv.repeat in reader: 

929 countValue = reader[conv.repeat] + conv.aux 

930 else: 

931 return NotImplemented 

932 totalSize += size * countValue 

933 return totalSize 

934 

935 def getConverters(self): 

936 return self.converters 

937 

938 def getConverterByName(self, name): 

939 return self.convertersByName[name] 

940 

941 def populateDefaults(self, propagator=None): 

942 for conv in self.getConverters(): 

943 if conv.repeat: 

944 if not hasattr(self, conv.name): 

945 setattr(self, conv.name, []) 

946 countValue = len(getattr(self, conv.name)) - conv.aux 

947 try: 

948 count_conv = self.getConverterByName(conv.repeat) 

949 setattr(self, conv.repeat, countValue) 

950 except KeyError: 

951 # conv.repeat is a propagated count 

952 if propagator and conv.repeat in propagator: 

953 propagator[conv.repeat].setValue(countValue) 

954 else: 

955 if conv.aux and not eval(conv.aux, None, self.__dict__): 

956 continue 

957 if hasattr(self, conv.name): 

958 continue # Warn if it should NOT be present?! 

959 if hasattr(conv, "writeNullOffset"): 

960 setattr(self, conv.name, None) # Warn? 

961 # elif not conv.isCount: 

962 # # Warn? 

963 # pass 

964 if hasattr(conv, "DEFAULT"): 

965 # OptionalValue converters (e.g. VarIndex) 

966 setattr(self, conv.name, conv.DEFAULT) 

967 

968 def decompile(self, reader, font): 

969 self.readFormat(reader) 

970 table = {} 

971 self.__rawTable = table # for debugging 

972 for conv in self.getConverters(): 

973 if conv.name == "SubTable": 

974 conv = conv.getConverter(reader.tableTag, table["LookupType"]) 

975 if conv.name == "ExtSubTable": 

976 conv = conv.getConverter(reader.tableTag, table["ExtensionLookupType"]) 

977 if conv.name == "FeatureParams": 

978 conv = conv.getConverter(reader["FeatureTag"]) 

979 if conv.name == "SubStruct": 

980 conv = conv.getConverter(reader.tableTag, table["MorphType"]) 

981 try: 

982 if conv.repeat: 

983 if isinstance(conv.repeat, int): 

984 countValue = conv.repeat 

985 elif conv.repeat in table: 

986 countValue = table[conv.repeat] 

987 else: 

988 # conv.repeat is a propagated count 

989 countValue = reader[conv.repeat] 

990 countValue += conv.aux 

991 table[conv.name] = conv.readArray(reader, font, table, countValue) 

992 else: 

993 if conv.aux and not eval(conv.aux, None, table): 

994 continue 

995 table[conv.name] = conv.read(reader, font, table) 

996 if conv.isPropagated: 

997 reader[conv.name] = table[conv.name] 

998 except Exception as e: 

999 name = conv.name 

1000 e.args = e.args + (name,) 

1001 raise 

1002 

1003 if hasattr(self, "postRead"): 

1004 self.postRead(table, font) 

1005 else: 

1006 self.__dict__.update(table) 

1007 

1008 del self.__rawTable # succeeded, get rid of debugging info 

1009 

1010 def compile(self, writer, font): 

1011 self.ensureDecompiled() 

1012 # TODO Following hack to be removed by rewriting how FormatSwitching tables 

1013 # are handled. 

1014 # https://github.com/fonttools/fonttools/pull/2238#issuecomment-805192631 

1015 if hasattr(self, "preWrite"): 

1016 deleteFormat = not hasattr(self, "Format") 

1017 table = self.preWrite(font) 

1018 deleteFormat = deleteFormat and hasattr(self, "Format") 

1019 else: 

1020 deleteFormat = False 

1021 table = self.__dict__.copy() 

1022 

1023 # some count references may have been initialized in a custom preWrite; we set 

1024 # these in the writer's state beforehand (instead of sequentially) so they will 

1025 # be propagated to all nested subtables even if the count appears in the current 

1026 # table only *after* the offset to the subtable that it is counting. 

1027 for conv in self.getConverters(): 

1028 if conv.isCount and conv.isPropagated: 

1029 value = table.get(conv.name) 

1030 if isinstance(value, CountReference): 

1031 writer[conv.name] = value 

1032 

1033 if hasattr(self, "sortCoverageLast"): 

1034 writer.sortCoverageLast = 1 

1035 

1036 if hasattr(self, "DontShare"): 

1037 writer.DontShare = True 

1038 

1039 if hasattr(self.__class__, "LookupType"): 

1040 writer["LookupType"].setValue(self.__class__.LookupType) 

1041 

1042 self.writeFormat(writer) 

1043 for conv in self.getConverters(): 

1044 value = table.get( 

1045 conv.name 

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

1047 if conv.repeat: 

1048 if value is None: 

1049 value = [] 

1050 countValue = len(value) - conv.aux 

1051 if isinstance(conv.repeat, int): 

1052 assert len(value) == conv.repeat, "expected %d values, got %d" % ( 

1053 conv.repeat, 

1054 len(value), 

1055 ) 

1056 elif conv.repeat in table: 

1057 CountReference(table, conv.repeat, value=countValue) 

1058 else: 

1059 # conv.repeat is a propagated count 

1060 writer[conv.repeat].setValue(countValue) 

1061 try: 

1062 conv.writeArray(writer, font, table, value) 

1063 except Exception as e: 

1064 e.args = e.args + (conv.name + "[]",) 

1065 raise 

1066 elif conv.isCount: 

1067 # Special-case Count values. 

1068 # Assumption: a Count field will *always* precede 

1069 # the actual array(s). 

1070 # We need a default value, as it may be set later by a nested 

1071 # table. We will later store it here. 

1072 # We add a reference: by the time the data is assembled 

1073 # the Count value will be filled in. 

1074 # We ignore the current count value since it will be recomputed, 

1075 # unless it's a CountReference that was already initialized in a custom preWrite. 

1076 if isinstance(value, CountReference): 

1077 ref = value 

1078 ref.size = conv.staticSize 

1079 writer.writeData(ref) 

1080 table[conv.name] = ref.getValue() 

1081 else: 

1082 ref = writer.writeCountReference(table, conv.name, conv.staticSize) 

1083 table[conv.name] = None 

1084 if conv.isPropagated: 

1085 writer[conv.name] = ref 

1086 elif conv.isLookupType: 

1087 # We make sure that subtables have the same lookup type, 

1088 # and that the type is the same as the one set on the 

1089 # Lookup object, if any is set. 

1090 if conv.name not in table: 

1091 table[conv.name] = None 

1092 ref = writer.writeCountReference( 

1093 table, conv.name, conv.staticSize, table[conv.name] 

1094 ) 

1095 writer["LookupType"] = ref 

1096 else: 

1097 if conv.aux and not eval(conv.aux, None, table): 

1098 continue 

1099 try: 

1100 conv.write(writer, font, table, value) 

1101 except Exception as e: 

1102 name = value.__class__.__name__ if value is not None else conv.name 

1103 e.args = e.args + (name,) 

1104 raise 

1105 if conv.isPropagated: 

1106 writer[conv.name] = value 

1107 

1108 if deleteFormat: 

1109 del self.Format 

1110 

1111 def readFormat(self, reader): 

1112 pass 

1113 

1114 def writeFormat(self, writer): 

1115 pass 

1116 

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

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

1119 if attrs is None: 

1120 attrs = [] 

1121 if hasattr(self, "Format"): 

1122 attrs = attrs + [("Format", self.Format)] 

1123 xmlWriter.begintag(tableName, attrs) 

1124 xmlWriter.newline() 

1125 self.toXML2(xmlWriter, font) 

1126 xmlWriter.endtag(tableName) 

1127 xmlWriter.newline() 

1128 

1129 def toXML2(self, xmlWriter, font): 

1130 # Simpler variant of toXML, *only* for the top level tables (like GPOS, GSUB). 

1131 # This is because in TTX our parent writes our main tag, and in otBase.py we 

1132 # do it ourselves. I think I'm getting schizophrenic... 

1133 for conv in self.getConverters(): 

1134 if conv.repeat: 

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

1136 for i in range(len(value)): 

1137 item = value[i] 

1138 conv.xmlWrite(xmlWriter, font, item, conv.name, [("index", i)]) 

1139 else: 

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

1141 continue 

1142 value = getattr( 

1143 self, conv.name, None 

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

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

1146 

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

1148 try: 

1149 conv = self.getConverterByName(name) 

1150 except KeyError: 

1151 raise # XXX on KeyError, raise nice error 

1152 value = conv.xmlRead(attrs, content, font) 

1153 if conv.repeat: 

1154 seq = getattr(self, conv.name, None) 

1155 if seq is None: 

1156 seq = [] 

1157 setattr(self, conv.name, seq) 

1158 seq.append(value) 

1159 else: 

1160 setattr(self, conv.name, value) 

1161 

1162 def __ne__(self, other): 

1163 result = self.__eq__(other) 

1164 return result if result is NotImplemented else not result 

1165 

1166 def __eq__(self, other): 

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

1168 return NotImplemented 

1169 

1170 self.ensureDecompiled() 

1171 other.ensureDecompiled() 

1172 

1173 return self.__dict__ == other.__dict__ 

1174 

1175 class SubTableEntry(NamedTuple): 

1176 """See BaseTable.iterSubTables()""" 

1177 

1178 name: str 

1179 value: "BaseTable" 

1180 index: Optional[int] = None # index into given array, None for single values 

1181 

1182 def iterSubTables(self) -> Iterator[SubTableEntry]: 

1183 """Yield (name, value, index) namedtuples for all subtables of current table. 

1184 

1185 A sub-table is an instance of BaseTable (or subclass thereof) that is a child 

1186 of self, the current parent table. 

1187 The tuples also contain the attribute name (str) of the of parent table to get 

1188 a subtable, and optionally, for lists of subtables (i.e. attributes associated 

1189 with a converter that has a 'repeat'), an index into the list containing the 

1190 given subtable value. 

1191 This method can be useful to traverse trees of otTables. 

1192 """ 

1193 for conv in self.getConverters(): 

1194 name = conv.name 

1195 value = getattr(self, name, None) 

1196 if value is None: 

1197 continue 

1198 if isinstance(value, BaseTable): 

1199 yield self.SubTableEntry(name, value) 

1200 elif isinstance(value, list): 

1201 yield from ( 

1202 self.SubTableEntry(name, v, index=i) 

1203 for i, v in enumerate(value) 

1204 if isinstance(v, BaseTable) 

1205 ) 

1206 

1207 # instance (not @class)method for consistency with FormatSwitchingBaseTable 

1208 def getVariableAttrs(self): 

1209 return getVariableAttrs(self.__class__) 

1210 

1211 

1212class FormatSwitchingBaseTable(BaseTable): 

1213 

1214 """Minor specialization of BaseTable, for tables that have multiple 

1215 formats, eg. CoverageFormat1 vs. CoverageFormat2.""" 

1216 

1217 @classmethod 

1218 def getRecordSize(cls, reader): 

1219 return NotImplemented 

1220 

1221 def getConverters(self): 

1222 try: 

1223 fmt = self.Format 

1224 except AttributeError: 

1225 # some FormatSwitchingBaseTables (e.g. Coverage) no longer have 'Format' 

1226 # attribute after fully decompiled, only gain one in preWrite before being 

1227 # recompiled. In the decompiled state, these hand-coded classes defined in 

1228 # otTables.py lose their format-specific nature and gain more high-level 

1229 # attributes that are not tied to converters. 

1230 return [] 

1231 return self.converters.get(self.Format, []) 

1232 

1233 def getConverterByName(self, name): 

1234 return self.convertersByName[self.Format][name] 

1235 

1236 def readFormat(self, reader): 

1237 self.Format = reader.readUShort() 

1238 

1239 def writeFormat(self, writer): 

1240 writer.writeUShort(self.Format) 

1241 

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

1243 BaseTable.toXML(self, xmlWriter, font, attrs, name) 

1244 

1245 def getVariableAttrs(self): 

1246 return getVariableAttrs(self.__class__, self.Format) 

1247 

1248 

1249class UInt8FormatSwitchingBaseTable(FormatSwitchingBaseTable): 

1250 def readFormat(self, reader): 

1251 self.Format = reader.readUInt8() 

1252 

1253 def writeFormat(self, writer): 

1254 writer.writeUInt8(self.Format) 

1255 

1256 

1257formatSwitchingBaseTables = { 

1258 "uint16": FormatSwitchingBaseTable, 

1259 "uint8": UInt8FormatSwitchingBaseTable, 

1260} 

1261 

1262 

1263def getFormatSwitchingBaseTableClass(formatType): 

1264 try: 

1265 return formatSwitchingBaseTables[formatType] 

1266 except KeyError: 

1267 raise TypeError(f"Unsupported format type: {formatType!r}") 

1268 

1269 

1270# memoize since these are parsed from otData.py, thus stay constant 

1271@lru_cache() 

1272def getVariableAttrs(cls: BaseTable, fmt: Optional[int] = None) -> Tuple[str]: 

1273 """Return sequence of variable table field names (can be empty). 

1274 

1275 Attributes are deemed "variable" when their otData.py's description contain 

1276 'VarIndexBase + {offset}', e.g. COLRv1 PaintVar* tables. 

1277 """ 

1278 if not issubclass(cls, BaseTable): 

1279 raise TypeError(cls) 

1280 if issubclass(cls, FormatSwitchingBaseTable): 

1281 if fmt is None: 

1282 raise TypeError(f"'fmt' is required for format-switching {cls.__name__}") 

1283 converters = cls.convertersByName[fmt] 

1284 else: 

1285 converters = cls.convertersByName 

1286 # assume if no 'VarIndexBase' field is present, table has no variable fields 

1287 if "VarIndexBase" not in converters: 

1288 return () 

1289 varAttrs = {} 

1290 for name, conv in converters.items(): 

1291 offset = conv.getVarIndexOffset() 

1292 if offset is not None: 

1293 varAttrs[name] = offset 

1294 return tuple(sorted(varAttrs, key=varAttrs.__getitem__)) 

1295 

1296 

1297# 

1298# Support for ValueRecords 

1299# 

1300# This data type is so different from all other OpenType data types that 

1301# it requires quite a bit of code for itself. It even has special support 

1302# in OTTableReader and OTTableWriter... 

1303# 

1304 

1305valueRecordFormat = [ 

1306 # Mask Name isDevice signed 

1307 (0x0001, "XPlacement", 0, 1), 

1308 (0x0002, "YPlacement", 0, 1), 

1309 (0x0004, "XAdvance", 0, 1), 

1310 (0x0008, "YAdvance", 0, 1), 

1311 (0x0010, "XPlaDevice", 1, 0), 

1312 (0x0020, "YPlaDevice", 1, 0), 

1313 (0x0040, "XAdvDevice", 1, 0), 

1314 (0x0080, "YAdvDevice", 1, 0), 

1315 # reserved: 

1316 (0x0100, "Reserved1", 0, 0), 

1317 (0x0200, "Reserved2", 0, 0), 

1318 (0x0400, "Reserved3", 0, 0), 

1319 (0x0800, "Reserved4", 0, 0), 

1320 (0x1000, "Reserved5", 0, 0), 

1321 (0x2000, "Reserved6", 0, 0), 

1322 (0x4000, "Reserved7", 0, 0), 

1323 (0x8000, "Reserved8", 0, 0), 

1324] 

1325 

1326 

1327def _buildDict(): 

1328 d = {} 

1329 for mask, name, isDevice, signed in valueRecordFormat: 

1330 d[name] = mask, isDevice, signed 

1331 return d 

1332 

1333 

1334valueRecordFormatDict = _buildDict() 

1335 

1336 

1337class ValueRecordFactory(object): 

1338 

1339 """Given a format code, this object convert ValueRecords.""" 

1340 

1341 def __init__(self, valueFormat): 

1342 format = [] 

1343 for mask, name, isDevice, signed in valueRecordFormat: 

1344 if valueFormat & mask: 

1345 format.append((name, isDevice, signed)) 

1346 self.format = format 

1347 

1348 def __len__(self): 

1349 return len(self.format) 

1350 

1351 def readValueRecord(self, reader, font): 

1352 format = self.format 

1353 if not format: 

1354 return None 

1355 valueRecord = ValueRecord() 

1356 for name, isDevice, signed in format: 

1357 if signed: 

1358 value = reader.readShort() 

1359 else: 

1360 value = reader.readUShort() 

1361 if isDevice: 

1362 if value: 

1363 from . import otTables 

1364 

1365 subReader = reader.getSubReader(value) 

1366 value = getattr(otTables, name)() 

1367 value.decompile(subReader, font) 

1368 else: 

1369 value = None 

1370 setattr(valueRecord, name, value) 

1371 return valueRecord 

1372 

1373 def writeValueRecord(self, writer, font, valueRecord): 

1374 for name, isDevice, signed in self.format: 

1375 value = getattr(valueRecord, name, 0) 

1376 if isDevice: 

1377 if value: 

1378 subWriter = writer.getSubWriter() 

1379 writer.writeSubTable(subWriter, offsetSize=2) 

1380 value.compile(subWriter, font) 

1381 else: 

1382 writer.writeUShort(0) 

1383 elif signed: 

1384 writer.writeShort(value) 

1385 else: 

1386 writer.writeUShort(value) 

1387 

1388 

1389class ValueRecord(object): 

1390 # see ValueRecordFactory 

1391 

1392 def __init__(self, valueFormat=None, src=None): 

1393 if valueFormat is not None: 

1394 for mask, name, isDevice, signed in valueRecordFormat: 

1395 if valueFormat & mask: 

1396 setattr(self, name, None if isDevice else 0) 

1397 if src is not None: 

1398 for key, val in src.__dict__.items(): 

1399 if not hasattr(self, key): 

1400 continue 

1401 setattr(self, key, val) 

1402 elif src is not None: 

1403 self.__dict__ = src.__dict__.copy() 

1404 

1405 def getFormat(self): 

1406 format = 0 

1407 for name in self.__dict__.keys(): 

1408 format = format | valueRecordFormatDict[name][0] 

1409 return format 

1410 

1411 def getEffectiveFormat(self): 

1412 format = 0 

1413 for name, value in self.__dict__.items(): 

1414 if value: 

1415 format = format | valueRecordFormatDict[name][0] 

1416 return format 

1417 

1418 def toXML(self, xmlWriter, font, valueName, attrs=None): 

1419 if attrs is None: 

1420 simpleItems = [] 

1421 else: 

1422 simpleItems = list(attrs) 

1423 for mask, name, isDevice, format in valueRecordFormat[:4]: # "simple" values 

1424 if hasattr(self, name): 

1425 simpleItems.append((name, getattr(self, name))) 

1426 deviceItems = [] 

1427 for mask, name, isDevice, format in valueRecordFormat[4:8]: # device records 

1428 if hasattr(self, name): 

1429 device = getattr(self, name) 

1430 if device is not None: 

1431 deviceItems.append((name, device)) 

1432 if deviceItems: 

1433 xmlWriter.begintag(valueName, simpleItems) 

1434 xmlWriter.newline() 

1435 for name, deviceRecord in deviceItems: 

1436 if deviceRecord is not None: 

1437 deviceRecord.toXML(xmlWriter, font, name=name) 

1438 xmlWriter.endtag(valueName) 

1439 xmlWriter.newline() 

1440 else: 

1441 xmlWriter.simpletag(valueName, simpleItems) 

1442 xmlWriter.newline() 

1443 

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

1445 from . import otTables 

1446 

1447 for k, v in attrs.items(): 

1448 setattr(self, k, int(v)) 

1449 for element in content: 

1450 if not isinstance(element, tuple): 

1451 continue 

1452 name, attrs, content = element 

1453 value = getattr(otTables, name)() 

1454 for elem2 in content: 

1455 if not isinstance(elem2, tuple): 

1456 continue 

1457 name2, attrs2, content2 = elem2 

1458 value.fromXML(name2, attrs2, content2, font) 

1459 setattr(self, name, value) 

1460 

1461 def __ne__(self, other): 

1462 result = self.__eq__(other) 

1463 return result if result is NotImplemented else not result 

1464 

1465 def __eq__(self, other): 

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

1467 return NotImplemented 

1468 return self.__dict__ == other.__dict__