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

1304 statements  

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

1from fontTools.misc.fixedTools import ( 

2 fixedToFloat as fi2fl, 

3 floatToFixed as fl2fi, 

4 floatToFixedToStr as fl2str, 

5 strToFixedToFloat as str2fl, 

6 ensureVersionIsLong as fi2ve, 

7 versionToFixed as ve2fi, 

8) 

9from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound 

10from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval 

11from fontTools.ttLib import getSearchRange 

12from .otBase import ( 

13 CountReference, 

14 FormatSwitchingBaseTable, 

15 OTTableReader, 

16 OTTableWriter, 

17 ValueRecordFactory, 

18) 

19from .otTables import ( 

20 lookupTypes, 

21 AATStateTable, 

22 AATState, 

23 AATAction, 

24 ContextualMorphAction, 

25 LigatureMorphAction, 

26 InsertionMorphAction, 

27 MorxSubtable, 

28 ExtendMode as _ExtendMode, 

29 CompositeMode as _CompositeMode, 

30 NO_VARIATION_INDEX, 

31) 

32from itertools import zip_longest 

33from functools import partial 

34import re 

35import struct 

36from typing import Optional 

37import logging 

38 

39 

40log = logging.getLogger(__name__) 

41istuple = lambda t: isinstance(t, tuple) 

42 

43 

44def buildConverters(tableSpec, tableNamespace): 

45 """Given a table spec from otData.py, build a converter object for each 

46 field of the table. This is called for each table in otData.py, and 

47 the results are assigned to the corresponding class in otTables.py.""" 

48 converters = [] 

49 convertersByName = {} 

50 for tp, name, repeat, aux, descr in tableSpec: 

51 tableName = name 

52 if name.startswith("ValueFormat"): 

53 assert tp == "uint16" 

54 converterClass = ValueFormat 

55 elif name.endswith("Count") or name in ("StructLength", "MorphType"): 

56 converterClass = { 

57 "uint8": ComputedUInt8, 

58 "uint16": ComputedUShort, 

59 "uint32": ComputedULong, 

60 }[tp] 

61 elif name == "SubTable": 

62 converterClass = SubTable 

63 elif name == "ExtSubTable": 

64 converterClass = ExtSubTable 

65 elif name == "SubStruct": 

66 converterClass = SubStruct 

67 elif name == "FeatureParams": 

68 converterClass = FeatureParams 

69 elif name in ("CIDGlyphMapping", "GlyphCIDMapping"): 

70 converterClass = StructWithLength 

71 else: 

72 if not tp in converterMapping and "(" not in tp: 

73 tableName = tp 

74 converterClass = Struct 

75 else: 

76 converterClass = eval(tp, tableNamespace, converterMapping) 

77 

78 conv = converterClass(name, repeat, aux, description=descr) 

79 

80 if conv.tableClass: 

81 # A "template" such as OffsetTo(AType) knowss the table class already 

82 tableClass = conv.tableClass 

83 elif tp in ("MortChain", "MortSubtable", "MorxChain"): 

84 tableClass = tableNamespace.get(tp) 

85 else: 

86 tableClass = tableNamespace.get(tableName) 

87 

88 if not conv.tableClass: 

89 conv.tableClass = tableClass 

90 

91 if name in ["SubTable", "ExtSubTable", "SubStruct"]: 

92 conv.lookupTypes = tableNamespace["lookupTypes"] 

93 # also create reverse mapping 

94 for t in conv.lookupTypes.values(): 

95 for cls in t.values(): 

96 convertersByName[cls.__name__] = Table(name, repeat, aux, cls) 

97 if name == "FeatureParams": 

98 conv.featureParamTypes = tableNamespace["featureParamTypes"] 

99 conv.defaultFeatureParams = tableNamespace["FeatureParams"] 

100 for cls in conv.featureParamTypes.values(): 

101 convertersByName[cls.__name__] = Table(name, repeat, aux, cls) 

102 converters.append(conv) 

103 assert name not in convertersByName, name 

104 convertersByName[name] = conv 

105 return converters, convertersByName 

106 

107 

108class _MissingItem(tuple): 

109 __slots__ = () 

110 

111 

112try: 

113 from collections import UserList 

114except ImportError: 

115 from UserList import UserList 

116 

117 

118class _LazyList(UserList): 

119 def __getslice__(self, i, j): 

120 return self.__getitem__(slice(i, j)) 

121 

122 def __getitem__(self, k): 

123 if isinstance(k, slice): 

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

125 return [self[i] for i in indices] 

126 item = self.data[k] 

127 if isinstance(item, _MissingItem): 

128 self.reader.seek(self.pos + item[0] * self.recordSize) 

129 item = self.conv.read(self.reader, self.font, {}) 

130 self.data[k] = item 

131 return item 

132 

133 def __add__(self, other): 

134 if isinstance(other, _LazyList): 

135 other = list(other) 

136 elif isinstance(other, list): 

137 pass 

138 else: 

139 return NotImplemented 

140 return list(self) + other 

141 

142 def __radd__(self, other): 

143 if not isinstance(other, list): 

144 return NotImplemented 

145 return other + list(self) 

146 

147 

148class BaseConverter(object): 

149 

150 """Base class for converter objects. Apart from the constructor, this 

151 is an abstract class.""" 

152 

153 def __init__(self, name, repeat, aux, tableClass=None, *, description=""): 

154 self.name = name 

155 self.repeat = repeat 

156 self.aux = aux 

157 self.tableClass = tableClass 

158 self.isCount = name.endswith("Count") or name in [ 

159 "DesignAxisRecordSize", 

160 "ValueRecordSize", 

161 ] 

162 self.isLookupType = name.endswith("LookupType") or name == "MorphType" 

163 self.isPropagated = name in [ 

164 "ClassCount", 

165 "Class2Count", 

166 "FeatureTag", 

167 "SettingsCount", 

168 "VarRegionCount", 

169 "MappingCount", 

170 "RegionAxisCount", 

171 "DesignAxisCount", 

172 "DesignAxisRecordSize", 

173 "AxisValueCount", 

174 "ValueRecordSize", 

175 "AxisCount", 

176 "BaseGlyphRecordCount", 

177 "LayerRecordCount", 

178 ] 

179 self.description = description 

180 

181 def readArray(self, reader, font, tableDict, count): 

182 """Read an array of values from the reader.""" 

183 lazy = font.lazy and count > 8 

184 if lazy: 

185 recordSize = self.getRecordSize(reader) 

186 if recordSize is NotImplemented: 

187 lazy = False 

188 if not lazy: 

189 l = [] 

190 for i in range(count): 

191 l.append(self.read(reader, font, tableDict)) 

192 return l 

193 else: 

194 l = _LazyList() 

195 l.reader = reader.copy() 

196 l.pos = l.reader.pos 

197 l.font = font 

198 l.conv = self 

199 l.recordSize = recordSize 

200 l.extend(_MissingItem([i]) for i in range(count)) 

201 reader.advance(count * recordSize) 

202 return l 

203 

204 def getRecordSize(self, reader): 

205 if hasattr(self, "staticSize"): 

206 return self.staticSize 

207 return NotImplemented 

208 

209 def read(self, reader, font, tableDict): 

210 """Read a value from the reader.""" 

211 raise NotImplementedError(self) 

212 

213 def writeArray(self, writer, font, tableDict, values): 

214 try: 

215 for i, value in enumerate(values): 

216 self.write(writer, font, tableDict, value, i) 

217 except Exception as e: 

218 e.args = e.args + (i,) 

219 raise 

220 

221 def write(self, writer, font, tableDict, value, repeatIndex=None): 

222 """Write a value to the writer.""" 

223 raise NotImplementedError(self) 

224 

225 def xmlRead(self, attrs, content, font): 

226 """Read a value from XML.""" 

227 raise NotImplementedError(self) 

228 

229 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

230 """Write a value to XML.""" 

231 raise NotImplementedError(self) 

232 

233 varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)") 

234 

235 def getVarIndexOffset(self) -> Optional[int]: 

236 """If description has `VarIndexBase + {offset}`, return the offset else None.""" 

237 m = self.varIndexBasePlusOffsetRE.search(self.description) 

238 if not m: 

239 return None 

240 return int(m.group(1)) 

241 

242 

243class SimpleValue(BaseConverter): 

244 @staticmethod 

245 def toString(value): 

246 return value 

247 

248 @staticmethod 

249 def fromString(value): 

250 return value 

251 

252 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

253 xmlWriter.simpletag(name, attrs + [("value", self.toString(value))]) 

254 xmlWriter.newline() 

255 

256 def xmlRead(self, attrs, content, font): 

257 return self.fromString(attrs["value"]) 

258 

259 

260class OptionalValue(SimpleValue): 

261 DEFAULT = None 

262 

263 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

264 if value != self.DEFAULT: 

265 attrs.append(("value", self.toString(value))) 

266 xmlWriter.simpletag(name, attrs) 

267 xmlWriter.newline() 

268 

269 def xmlRead(self, attrs, content, font): 

270 if "value" in attrs: 

271 return self.fromString(attrs["value"]) 

272 return self.DEFAULT 

273 

274 

275class IntValue(SimpleValue): 

276 @staticmethod 

277 def fromString(value): 

278 return int(value, 0) 

279 

280 

281class Long(IntValue): 

282 staticSize = 4 

283 

284 def read(self, reader, font, tableDict): 

285 return reader.readLong() 

286 

287 def readArray(self, reader, font, tableDict, count): 

288 return reader.readLongArray(count) 

289 

290 def write(self, writer, font, tableDict, value, repeatIndex=None): 

291 writer.writeLong(value) 

292 

293 def writeArray(self, writer, font, tableDict, values): 

294 writer.writeLongArray(values) 

295 

296 

297class ULong(IntValue): 

298 staticSize = 4 

299 

300 def read(self, reader, font, tableDict): 

301 return reader.readULong() 

302 

303 def readArray(self, reader, font, tableDict, count): 

304 return reader.readULongArray(count) 

305 

306 def write(self, writer, font, tableDict, value, repeatIndex=None): 

307 writer.writeULong(value) 

308 

309 def writeArray(self, writer, font, tableDict, values): 

310 writer.writeULongArray(values) 

311 

312 

313class Flags32(ULong): 

314 @staticmethod 

315 def toString(value): 

316 return "0x%08X" % value 

317 

318 

319class VarIndex(OptionalValue, ULong): 

320 DEFAULT = NO_VARIATION_INDEX 

321 

322 

323class Short(IntValue): 

324 staticSize = 2 

325 

326 def read(self, reader, font, tableDict): 

327 return reader.readShort() 

328 

329 def readArray(self, reader, font, tableDict, count): 

330 return reader.readShortArray(count) 

331 

332 def write(self, writer, font, tableDict, value, repeatIndex=None): 

333 writer.writeShort(value) 

334 

335 def writeArray(self, writer, font, tableDict, values): 

336 writer.writeShortArray(values) 

337 

338 

339class UShort(IntValue): 

340 staticSize = 2 

341 

342 def read(self, reader, font, tableDict): 

343 return reader.readUShort() 

344 

345 def readArray(self, reader, font, tableDict, count): 

346 return reader.readUShortArray(count) 

347 

348 def write(self, writer, font, tableDict, value, repeatIndex=None): 

349 writer.writeUShort(value) 

350 

351 def writeArray(self, writer, font, tableDict, values): 

352 writer.writeUShortArray(values) 

353 

354 

355class Int8(IntValue): 

356 staticSize = 1 

357 

358 def read(self, reader, font, tableDict): 

359 return reader.readInt8() 

360 

361 def readArray(self, reader, font, tableDict, count): 

362 return reader.readInt8Array(count) 

363 

364 def write(self, writer, font, tableDict, value, repeatIndex=None): 

365 writer.writeInt8(value) 

366 

367 def writeArray(self, writer, font, tableDict, values): 

368 writer.writeInt8Array(values) 

369 

370 

371class UInt8(IntValue): 

372 staticSize = 1 

373 

374 def read(self, reader, font, tableDict): 

375 return reader.readUInt8() 

376 

377 def readArray(self, reader, font, tableDict, count): 

378 return reader.readUInt8Array(count) 

379 

380 def write(self, writer, font, tableDict, value, repeatIndex=None): 

381 writer.writeUInt8(value) 

382 

383 def writeArray(self, writer, font, tableDict, values): 

384 writer.writeUInt8Array(values) 

385 

386 

387class UInt24(IntValue): 

388 staticSize = 3 

389 

390 def read(self, reader, font, tableDict): 

391 return reader.readUInt24() 

392 

393 def write(self, writer, font, tableDict, value, repeatIndex=None): 

394 writer.writeUInt24(value) 

395 

396 

397class ComputedInt(IntValue): 

398 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

399 if value is not None: 

400 xmlWriter.comment("%s=%s" % (name, value)) 

401 xmlWriter.newline() 

402 

403 

404class ComputedUInt8(ComputedInt, UInt8): 

405 pass 

406 

407 

408class ComputedUShort(ComputedInt, UShort): 

409 pass 

410 

411 

412class ComputedULong(ComputedInt, ULong): 

413 pass 

414 

415 

416class Tag(SimpleValue): 

417 staticSize = 4 

418 

419 def read(self, reader, font, tableDict): 

420 return reader.readTag() 

421 

422 def write(self, writer, font, tableDict, value, repeatIndex=None): 

423 writer.writeTag(value) 

424 

425 

426class GlyphID(SimpleValue): 

427 staticSize = 2 

428 typecode = "H" 

429 

430 def readArray(self, reader, font, tableDict, count): 

431 return font.getGlyphNameMany( 

432 reader.readArray(self.typecode, self.staticSize, count) 

433 ) 

434 

435 def read(self, reader, font, tableDict): 

436 return font.getGlyphName(reader.readValue(self.typecode, self.staticSize)) 

437 

438 def writeArray(self, writer, font, tableDict, values): 

439 writer.writeArray(self.typecode, font.getGlyphIDMany(values)) 

440 

441 def write(self, writer, font, tableDict, value, repeatIndex=None): 

442 writer.writeValue(self.typecode, font.getGlyphID(value)) 

443 

444 

445class GlyphID32(GlyphID): 

446 staticSize = 4 

447 typecode = "L" 

448 

449 

450class NameID(UShort): 

451 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

452 xmlWriter.simpletag(name, attrs + [("value", value)]) 

453 if font and value: 

454 nameTable = font.get("name") 

455 if nameTable: 

456 name = nameTable.getDebugName(value) 

457 xmlWriter.write(" ") 

458 if name: 

459 xmlWriter.comment(name) 

460 else: 

461 xmlWriter.comment("missing from name table") 

462 log.warning("name id %d missing from name table" % value) 

463 xmlWriter.newline() 

464 

465 

466class STATFlags(UShort): 

467 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

468 xmlWriter.simpletag(name, attrs + [("value", value)]) 

469 flags = [] 

470 if value & 0x01: 

471 flags.append("OlderSiblingFontAttribute") 

472 if value & 0x02: 

473 flags.append("ElidableAxisValueName") 

474 if flags: 

475 xmlWriter.write(" ") 

476 xmlWriter.comment(" ".join(flags)) 

477 xmlWriter.newline() 

478 

479 

480class FloatValue(SimpleValue): 

481 @staticmethod 

482 def fromString(value): 

483 return float(value) 

484 

485 

486class DeciPoints(FloatValue): 

487 staticSize = 2 

488 

489 def read(self, reader, font, tableDict): 

490 return reader.readUShort() / 10 

491 

492 def write(self, writer, font, tableDict, value, repeatIndex=None): 

493 writer.writeUShort(round(value * 10)) 

494 

495 

496class BaseFixedValue(FloatValue): 

497 staticSize = NotImplemented 

498 precisionBits = NotImplemented 

499 readerMethod = NotImplemented 

500 writerMethod = NotImplemented 

501 

502 def read(self, reader, font, tableDict): 

503 return self.fromInt(getattr(reader, self.readerMethod)()) 

504 

505 def write(self, writer, font, tableDict, value, repeatIndex=None): 

506 getattr(writer, self.writerMethod)(self.toInt(value)) 

507 

508 @classmethod 

509 def fromInt(cls, value): 

510 return fi2fl(value, cls.precisionBits) 

511 

512 @classmethod 

513 def toInt(cls, value): 

514 return fl2fi(value, cls.precisionBits) 

515 

516 @classmethod 

517 def fromString(cls, value): 

518 return str2fl(value, cls.precisionBits) 

519 

520 @classmethod 

521 def toString(cls, value): 

522 return fl2str(value, cls.precisionBits) 

523 

524 

525class Fixed(BaseFixedValue): 

526 staticSize = 4 

527 precisionBits = 16 

528 readerMethod = "readLong" 

529 writerMethod = "writeLong" 

530 

531 

532class F2Dot14(BaseFixedValue): 

533 staticSize = 2 

534 precisionBits = 14 

535 readerMethod = "readShort" 

536 writerMethod = "writeShort" 

537 

538 

539class Angle(F2Dot14): 

540 # angles are specified in degrees, and encoded as F2Dot14 fractions of half 

541 # circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc. 

542 bias = 0.0 

543 factor = 1.0 / (1 << 14) * 180 # 0.010986328125 

544 

545 @classmethod 

546 def fromInt(cls, value): 

547 return (super().fromInt(value) + cls.bias) * 180 

548 

549 @classmethod 

550 def toInt(cls, value): 

551 return super().toInt((value / 180) - cls.bias) 

552 

553 @classmethod 

554 def fromString(cls, value): 

555 # quantize to nearest multiples of minimum fixed-precision angle 

556 return otRound(float(value) / cls.factor) * cls.factor 

557 

558 @classmethod 

559 def toString(cls, value): 

560 return nearestMultipleShortestRepr(value, cls.factor) 

561 

562 

563class BiasedAngle(Angle): 

564 # A bias of 1.0 is used in the representation of start and end angles 

565 # of COLRv1 PaintSweepGradients to allow for encoding +360deg 

566 bias = 1.0 

567 

568 

569class Version(SimpleValue): 

570 staticSize = 4 

571 

572 def read(self, reader, font, tableDict): 

573 value = reader.readLong() 

574 return value 

575 

576 def write(self, writer, font, tableDict, value, repeatIndex=None): 

577 value = fi2ve(value) 

578 writer.writeLong(value) 

579 

580 @staticmethod 

581 def fromString(value): 

582 return ve2fi(value) 

583 

584 @staticmethod 

585 def toString(value): 

586 return "0x%08x" % value 

587 

588 @staticmethod 

589 def fromFloat(v): 

590 return fl2fi(v, 16) 

591 

592 

593class Char64(SimpleValue): 

594 """An ASCII string with up to 64 characters. 

595 

596 Unused character positions are filled with 0x00 bytes. 

597 Used in Apple AAT fonts in the `gcid` table. 

598 """ 

599 

600 staticSize = 64 

601 

602 def read(self, reader, font, tableDict): 

603 data = reader.readData(self.staticSize) 

604 zeroPos = data.find(b"\0") 

605 if zeroPos >= 0: 

606 data = data[:zeroPos] 

607 s = tostr(data, encoding="ascii", errors="replace") 

608 if s != tostr(data, encoding="ascii", errors="ignore"): 

609 log.warning('replaced non-ASCII characters in "%s"' % s) 

610 return s 

611 

612 def write(self, writer, font, tableDict, value, repeatIndex=None): 

613 data = tobytes(value, encoding="ascii", errors="replace") 

614 if data != tobytes(value, encoding="ascii", errors="ignore"): 

615 log.warning('replacing non-ASCII characters in "%s"' % value) 

616 if len(data) > self.staticSize: 

617 log.warning( 

618 'truncating overlong "%s" to %d bytes' % (value, self.staticSize) 

619 ) 

620 data = (data + b"\0" * self.staticSize)[: self.staticSize] 

621 writer.writeData(data) 

622 

623 

624class Struct(BaseConverter): 

625 def getRecordSize(self, reader): 

626 return self.tableClass and self.tableClass.getRecordSize(reader) 

627 

628 def read(self, reader, font, tableDict): 

629 table = self.tableClass() 

630 table.decompile(reader, font) 

631 return table 

632 

633 def write(self, writer, font, tableDict, value, repeatIndex=None): 

634 value.compile(writer, font) 

635 

636 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

637 if value is None: 

638 if attrs: 

639 # If there are attributes (probably index), then 

640 # don't drop this even if it's NULL. It will mess 

641 # up the array indices of the containing element. 

642 xmlWriter.simpletag(name, attrs + [("empty", 1)]) 

643 xmlWriter.newline() 

644 else: 

645 pass # NULL table, ignore 

646 else: 

647 value.toXML(xmlWriter, font, attrs, name=name) 

648 

649 def xmlRead(self, attrs, content, font): 

650 if "empty" in attrs and safeEval(attrs["empty"]): 

651 return None 

652 table = self.tableClass() 

653 Format = attrs.get("Format") 

654 if Format is not None: 

655 table.Format = int(Format) 

656 

657 noPostRead = not hasattr(table, "postRead") 

658 if noPostRead: 

659 # TODO Cache table.hasPropagated. 

660 cleanPropagation = False 

661 for conv in table.getConverters(): 

662 if conv.isPropagated: 

663 cleanPropagation = True 

664 if not hasattr(font, "_propagator"): 

665 font._propagator = {} 

666 propagator = font._propagator 

667 assert conv.name not in propagator, (conv.name, propagator) 

668 setattr(table, conv.name, None) 

669 propagator[conv.name] = CountReference(table.__dict__, conv.name) 

670 

671 for element in content: 

672 if isinstance(element, tuple): 

673 name, attrs, content = element 

674 table.fromXML(name, attrs, content, font) 

675 else: 

676 pass 

677 

678 table.populateDefaults(propagator=getattr(font, "_propagator", None)) 

679 

680 if noPostRead: 

681 if cleanPropagation: 

682 for conv in table.getConverters(): 

683 if conv.isPropagated: 

684 propagator = font._propagator 

685 del propagator[conv.name] 

686 if not propagator: 

687 del font._propagator 

688 

689 return table 

690 

691 def __repr__(self): 

692 return "Struct of " + repr(self.tableClass) 

693 

694 

695class StructWithLength(Struct): 

696 def read(self, reader, font, tableDict): 

697 pos = reader.pos 

698 table = self.tableClass() 

699 table.decompile(reader, font) 

700 reader.seek(pos + table.StructLength) 

701 return table 

702 

703 def write(self, writer, font, tableDict, value, repeatIndex=None): 

704 for convIndex, conv in enumerate(value.getConverters()): 

705 if conv.name == "StructLength": 

706 break 

707 lengthIndex = len(writer.items) + convIndex 

708 if isinstance(value, FormatSwitchingBaseTable): 

709 lengthIndex += 1 # implicit Format field 

710 deadbeef = {1: 0xDE, 2: 0xDEAD, 4: 0xDEADBEEF}[conv.staticSize] 

711 

712 before = writer.getDataLength() 

713 value.StructLength = deadbeef 

714 value.compile(writer, font) 

715 length = writer.getDataLength() - before 

716 lengthWriter = writer.getSubWriter() 

717 conv.write(lengthWriter, font, tableDict, length) 

718 assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef"[: conv.staticSize] 

719 writer.items[lengthIndex] = lengthWriter.getAllData() 

720 

721 

722class Table(Struct): 

723 staticSize = 2 

724 

725 def readOffset(self, reader): 

726 return reader.readUShort() 

727 

728 def writeNullOffset(self, writer): 

729 writer.writeUShort(0) 

730 

731 def read(self, reader, font, tableDict): 

732 offset = self.readOffset(reader) 

733 if offset == 0: 

734 return None 

735 table = self.tableClass() 

736 reader = reader.getSubReader(offset) 

737 if font.lazy: 

738 table.reader = reader 

739 table.font = font 

740 else: 

741 table.decompile(reader, font) 

742 return table 

743 

744 def write(self, writer, font, tableDict, value, repeatIndex=None): 

745 if value is None: 

746 self.writeNullOffset(writer) 

747 else: 

748 subWriter = writer.getSubWriter() 

749 subWriter.name = self.name 

750 if repeatIndex is not None: 

751 subWriter.repeatIndex = repeatIndex 

752 writer.writeSubTable(subWriter, offsetSize=self.staticSize) 

753 value.compile(subWriter, font) 

754 

755 

756class LTable(Table): 

757 staticSize = 4 

758 

759 def readOffset(self, reader): 

760 return reader.readULong() 

761 

762 def writeNullOffset(self, writer): 

763 writer.writeULong(0) 

764 

765 

766# Table pointed to by a 24-bit, 3-byte long offset 

767class Table24(Table): 

768 staticSize = 3 

769 

770 def readOffset(self, reader): 

771 return reader.readUInt24() 

772 

773 def writeNullOffset(self, writer): 

774 writer.writeUInt24(0) 

775 

776 

777# TODO Clean / merge the SubTable and SubStruct 

778 

779 

780class SubStruct(Struct): 

781 def getConverter(self, tableType, lookupType): 

782 tableClass = self.lookupTypes[tableType][lookupType] 

783 return self.__class__(self.name, self.repeat, self.aux, tableClass) 

784 

785 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

786 super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs) 

787 

788 

789class SubTable(Table): 

790 def getConverter(self, tableType, lookupType): 

791 tableClass = self.lookupTypes[tableType][lookupType] 

792 return self.__class__(self.name, self.repeat, self.aux, tableClass) 

793 

794 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

795 super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs) 

796 

797 

798class ExtSubTable(LTable, SubTable): 

799 def write(self, writer, font, tableDict, value, repeatIndex=None): 

800 writer.Extension = True # actually, mere presence of the field flags it as an Ext Subtable writer. 

801 Table.write(self, writer, font, tableDict, value, repeatIndex) 

802 

803 

804class FeatureParams(Table): 

805 def getConverter(self, featureTag): 

806 tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams) 

807 return self.__class__(self.name, self.repeat, self.aux, tableClass) 

808 

809 

810class ValueFormat(IntValue): 

811 staticSize = 2 

812 

813 def __init__(self, name, repeat, aux, tableClass=None, *, description=""): 

814 BaseConverter.__init__( 

815 self, name, repeat, aux, tableClass, description=description 

816 ) 

817 self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1") 

818 

819 def read(self, reader, font, tableDict): 

820 format = reader.readUShort() 

821 reader[self.which] = ValueRecordFactory(format) 

822 return format 

823 

824 def write(self, writer, font, tableDict, format, repeatIndex=None): 

825 writer.writeUShort(format) 

826 writer[self.which] = ValueRecordFactory(format) 

827 

828 

829class ValueRecord(ValueFormat): 

830 def getRecordSize(self, reader): 

831 return 2 * len(reader[self.which]) 

832 

833 def read(self, reader, font, tableDict): 

834 return reader[self.which].readValueRecord(reader, font) 

835 

836 def write(self, writer, font, tableDict, value, repeatIndex=None): 

837 writer[self.which].writeValueRecord(writer, font, value) 

838 

839 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

840 if value is None: 

841 pass # NULL table, ignore 

842 else: 

843 value.toXML(xmlWriter, font, self.name, attrs) 

844 

845 def xmlRead(self, attrs, content, font): 

846 from .otBase import ValueRecord 

847 

848 value = ValueRecord() 

849 value.fromXML(None, attrs, content, font) 

850 return value 

851 

852 

853class AATLookup(BaseConverter): 

854 BIN_SEARCH_HEADER_SIZE = 10 

855 

856 def __init__(self, name, repeat, aux, tableClass, *, description=""): 

857 BaseConverter.__init__( 

858 self, name, repeat, aux, tableClass, description=description 

859 ) 

860 if issubclass(self.tableClass, SimpleValue): 

861 self.converter = self.tableClass(name="Value", repeat=None, aux=None) 

862 else: 

863 self.converter = Table( 

864 name="Value", repeat=None, aux=None, tableClass=self.tableClass 

865 ) 

866 

867 def read(self, reader, font, tableDict): 

868 format = reader.readUShort() 

869 if format == 0: 

870 return self.readFormat0(reader, font) 

871 elif format == 2: 

872 return self.readFormat2(reader, font) 

873 elif format == 4: 

874 return self.readFormat4(reader, font) 

875 elif format == 6: 

876 return self.readFormat6(reader, font) 

877 elif format == 8: 

878 return self.readFormat8(reader, font) 

879 else: 

880 assert False, "unsupported lookup format: %d" % format 

881 

882 def write(self, writer, font, tableDict, value, repeatIndex=None): 

883 values = list( 

884 sorted([(font.getGlyphID(glyph), val) for glyph, val in value.items()]) 

885 ) 

886 # TODO: Also implement format 4. 

887 formats = list( 

888 sorted( 

889 filter( 

890 None, 

891 [ 

892 self.buildFormat0(writer, font, values), 

893 self.buildFormat2(writer, font, values), 

894 self.buildFormat6(writer, font, values), 

895 self.buildFormat8(writer, font, values), 

896 ], 

897 ) 

898 ) 

899 ) 

900 # We use the format ID as secondary sort key to make the output 

901 # deterministic when multiple formats have same encoded size. 

902 dataSize, lookupFormat, writeMethod = formats[0] 

903 pos = writer.getDataLength() 

904 writeMethod() 

905 actualSize = writer.getDataLength() - pos 

906 assert ( 

907 actualSize == dataSize 

908 ), "AATLookup format %d claimed to write %d bytes, but wrote %d" % ( 

909 lookupFormat, 

910 dataSize, 

911 actualSize, 

912 ) 

913 

914 @staticmethod 

915 def writeBinSearchHeader(writer, numUnits, unitSize): 

916 writer.writeUShort(unitSize) 

917 writer.writeUShort(numUnits) 

918 searchRange, entrySelector, rangeShift = getSearchRange( 

919 n=numUnits, itemSize=unitSize 

920 ) 

921 writer.writeUShort(searchRange) 

922 writer.writeUShort(entrySelector) 

923 writer.writeUShort(rangeShift) 

924 

925 def buildFormat0(self, writer, font, values): 

926 numGlyphs = len(font.getGlyphOrder()) 

927 if len(values) != numGlyphs: 

928 return None 

929 valueSize = self.converter.staticSize 

930 return ( 

931 2 + numGlyphs * valueSize, 

932 0, 

933 lambda: self.writeFormat0(writer, font, values), 

934 ) 

935 

936 def writeFormat0(self, writer, font, values): 

937 writer.writeUShort(0) 

938 for glyphID_, value in values: 

939 self.converter.write( 

940 writer, font, tableDict=None, value=value, repeatIndex=None 

941 ) 

942 

943 def buildFormat2(self, writer, font, values): 

944 segStart, segValue = values[0] 

945 segEnd = segStart 

946 segments = [] 

947 for glyphID, curValue in values[1:]: 

948 if glyphID != segEnd + 1 or curValue != segValue: 

949 segments.append((segStart, segEnd, segValue)) 

950 segStart = segEnd = glyphID 

951 segValue = curValue 

952 else: 

953 segEnd = glyphID 

954 segments.append((segStart, segEnd, segValue)) 

955 valueSize = self.converter.staticSize 

956 numUnits, unitSize = len(segments) + 1, valueSize + 4 

957 return ( 

958 2 + self.BIN_SEARCH_HEADER_SIZE + numUnits * unitSize, 

959 2, 

960 lambda: self.writeFormat2(writer, font, segments), 

961 ) 

962 

963 def writeFormat2(self, writer, font, segments): 

964 writer.writeUShort(2) 

965 valueSize = self.converter.staticSize 

966 numUnits, unitSize = len(segments), valueSize + 4 

967 self.writeBinSearchHeader(writer, numUnits, unitSize) 

968 for firstGlyph, lastGlyph, value in segments: 

969 writer.writeUShort(lastGlyph) 

970 writer.writeUShort(firstGlyph) 

971 self.converter.write( 

972 writer, font, tableDict=None, value=value, repeatIndex=None 

973 ) 

974 writer.writeUShort(0xFFFF) 

975 writer.writeUShort(0xFFFF) 

976 writer.writeData(b"\x00" * valueSize) 

977 

978 def buildFormat6(self, writer, font, values): 

979 valueSize = self.converter.staticSize 

980 numUnits, unitSize = len(values), valueSize + 2 

981 return ( 

982 2 + self.BIN_SEARCH_HEADER_SIZE + (numUnits + 1) * unitSize, 

983 6, 

984 lambda: self.writeFormat6(writer, font, values), 

985 ) 

986 

987 def writeFormat6(self, writer, font, values): 

988 writer.writeUShort(6) 

989 valueSize = self.converter.staticSize 

990 numUnits, unitSize = len(values), valueSize + 2 

991 self.writeBinSearchHeader(writer, numUnits, unitSize) 

992 for glyphID, value in values: 

993 writer.writeUShort(glyphID) 

994 self.converter.write( 

995 writer, font, tableDict=None, value=value, repeatIndex=None 

996 ) 

997 writer.writeUShort(0xFFFF) 

998 writer.writeData(b"\x00" * valueSize) 

999 

1000 def buildFormat8(self, writer, font, values): 

1001 minGlyphID, maxGlyphID = values[0][0], values[-1][0] 

1002 if len(values) != maxGlyphID - minGlyphID + 1: 

1003 return None 

1004 valueSize = self.converter.staticSize 

1005 return ( 

1006 6 + len(values) * valueSize, 

1007 8, 

1008 lambda: self.writeFormat8(writer, font, values), 

1009 ) 

1010 

1011 def writeFormat8(self, writer, font, values): 

1012 firstGlyphID = values[0][0] 

1013 writer.writeUShort(8) 

1014 writer.writeUShort(firstGlyphID) 

1015 writer.writeUShort(len(values)) 

1016 for _, value in values: 

1017 self.converter.write( 

1018 writer, font, tableDict=None, value=value, repeatIndex=None 

1019 ) 

1020 

1021 def readFormat0(self, reader, font): 

1022 numGlyphs = len(font.getGlyphOrder()) 

1023 data = self.converter.readArray(reader, font, tableDict=None, count=numGlyphs) 

1024 return {font.getGlyphName(k): value for k, value in enumerate(data)} 

1025 

1026 def readFormat2(self, reader, font): 

1027 mapping = {} 

1028 pos = reader.pos - 2 # start of table is at UShort for format 

1029 unitSize, numUnits = reader.readUShort(), reader.readUShort() 

1030 assert unitSize >= 4 + self.converter.staticSize, unitSize 

1031 for i in range(numUnits): 

1032 reader.seek(pos + i * unitSize + 12) 

1033 last = reader.readUShort() 

1034 first = reader.readUShort() 

1035 value = self.converter.read(reader, font, tableDict=None) 

1036 if last != 0xFFFF: 

1037 for k in range(first, last + 1): 

1038 mapping[font.getGlyphName(k)] = value 

1039 return mapping 

1040 

1041 def readFormat4(self, reader, font): 

1042 mapping = {} 

1043 pos = reader.pos - 2 # start of table is at UShort for format 

1044 unitSize = reader.readUShort() 

1045 assert unitSize >= 6, unitSize 

1046 for i in range(reader.readUShort()): 

1047 reader.seek(pos + i * unitSize + 12) 

1048 last = reader.readUShort() 

1049 first = reader.readUShort() 

1050 offset = reader.readUShort() 

1051 if last != 0xFFFF: 

1052 dataReader = reader.getSubReader(0) # relative to current position 

1053 dataReader.seek(pos + offset) # relative to start of table 

1054 data = self.converter.readArray( 

1055 dataReader, font, tableDict=None, count=last - first + 1 

1056 ) 

1057 for k, v in enumerate(data): 

1058 mapping[font.getGlyphName(first + k)] = v 

1059 return mapping 

1060 

1061 def readFormat6(self, reader, font): 

1062 mapping = {} 

1063 pos = reader.pos - 2 # start of table is at UShort for format 

1064 unitSize = reader.readUShort() 

1065 assert unitSize >= 2 + self.converter.staticSize, unitSize 

1066 for i in range(reader.readUShort()): 

1067 reader.seek(pos + i * unitSize + 12) 

1068 glyphID = reader.readUShort() 

1069 value = self.converter.read(reader, font, tableDict=None) 

1070 if glyphID != 0xFFFF: 

1071 mapping[font.getGlyphName(glyphID)] = value 

1072 return mapping 

1073 

1074 def readFormat8(self, reader, font): 

1075 first = reader.readUShort() 

1076 count = reader.readUShort() 

1077 data = self.converter.readArray(reader, font, tableDict=None, count=count) 

1078 return {font.getGlyphName(first + k): value for (k, value) in enumerate(data)} 

1079 

1080 def xmlRead(self, attrs, content, font): 

1081 value = {} 

1082 for element in content: 

1083 if isinstance(element, tuple): 

1084 name, a, eltContent = element 

1085 if name == "Lookup": 

1086 value[a["glyph"]] = self.converter.xmlRead(a, eltContent, font) 

1087 return value 

1088 

1089 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1090 xmlWriter.begintag(name, attrs) 

1091 xmlWriter.newline() 

1092 for glyph, value in sorted(value.items()): 

1093 self.converter.xmlWrite( 

1094 xmlWriter, font, value=value, name="Lookup", attrs=[("glyph", glyph)] 

1095 ) 

1096 xmlWriter.endtag(name) 

1097 xmlWriter.newline() 

1098 

1099 

1100# The AAT 'ankr' table has an unusual structure: An offset to an AATLookup 

1101# followed by an offset to a glyph data table. Other than usual, the 

1102# offsets in the AATLookup are not relative to the beginning of 

1103# the beginning of the 'ankr' table, but relative to the glyph data table. 

1104# So, to find the anchor data for a glyph, one needs to add the offset 

1105# to the data table to the offset found in the AATLookup, and then use 

1106# the sum of these two offsets to find the actual data. 

1107class AATLookupWithDataOffset(BaseConverter): 

1108 def read(self, reader, font, tableDict): 

1109 lookupOffset = reader.readULong() 

1110 dataOffset = reader.readULong() 

1111 lookupReader = reader.getSubReader(lookupOffset) 

1112 lookup = AATLookup("DataOffsets", None, None, UShort) 

1113 offsets = lookup.read(lookupReader, font, tableDict) 

1114 result = {} 

1115 for glyph, offset in offsets.items(): 

1116 dataReader = reader.getSubReader(offset + dataOffset) 

1117 item = self.tableClass() 

1118 item.decompile(dataReader, font) 

1119 result[glyph] = item 

1120 return result 

1121 

1122 def write(self, writer, font, tableDict, value, repeatIndex=None): 

1123 # We do not work with OTTableWriter sub-writers because 

1124 # the offsets in our AATLookup are relative to our data 

1125 # table, for which we need to provide an offset value itself. 

1126 # It might have been possible to somehow make a kludge for 

1127 # performing this indirect offset computation directly inside 

1128 # OTTableWriter. But this would have made the internal logic 

1129 # of OTTableWriter even more complex than it already is, 

1130 # so we decided to roll our own offset computation for the 

1131 # contents of the AATLookup and associated data table. 

1132 offsetByGlyph, offsetByData, dataLen = {}, {}, 0 

1133 compiledData = [] 

1134 for glyph in sorted(value, key=font.getGlyphID): 

1135 subWriter = OTTableWriter() 

1136 value[glyph].compile(subWriter, font) 

1137 data = subWriter.getAllData() 

1138 offset = offsetByData.get(data, None) 

1139 if offset == None: 

1140 offset = dataLen 

1141 dataLen = dataLen + len(data) 

1142 offsetByData[data] = offset 

1143 compiledData.append(data) 

1144 offsetByGlyph[glyph] = offset 

1145 # For calculating the offsets to our AATLookup and data table, 

1146 # we can use the regular OTTableWriter infrastructure. 

1147 lookupWriter = writer.getSubWriter() 

1148 lookup = AATLookup("DataOffsets", None, None, UShort) 

1149 lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None) 

1150 

1151 dataWriter = writer.getSubWriter() 

1152 writer.writeSubTable(lookupWriter, offsetSize=4) 

1153 writer.writeSubTable(dataWriter, offsetSize=4) 

1154 for d in compiledData: 

1155 dataWriter.writeData(d) 

1156 

1157 def xmlRead(self, attrs, content, font): 

1158 lookup = AATLookup("DataOffsets", None, None, self.tableClass) 

1159 return lookup.xmlRead(attrs, content, font) 

1160 

1161 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1162 lookup = AATLookup("DataOffsets", None, None, self.tableClass) 

1163 lookup.xmlWrite(xmlWriter, font, value, name, attrs) 

1164 

1165 

1166class MorxSubtableConverter(BaseConverter): 

1167 _PROCESSING_ORDERS = { 

1168 # bits 30 and 28 of morx.CoverageFlags; see morx spec 

1169 (False, False): "LayoutOrder", 

1170 (True, False): "ReversedLayoutOrder", 

1171 (False, True): "LogicalOrder", 

1172 (True, True): "ReversedLogicalOrder", 

1173 } 

1174 

1175 _PROCESSING_ORDERS_REVERSED = {val: key for key, val in _PROCESSING_ORDERS.items()} 

1176 

1177 def __init__(self, name, repeat, aux, tableClass=None, *, description=""): 

1178 BaseConverter.__init__( 

1179 self, name, repeat, aux, tableClass, description=description 

1180 ) 

1181 

1182 def _setTextDirectionFromCoverageFlags(self, flags, subtable): 

1183 if (flags & 0x20) != 0: 

1184 subtable.TextDirection = "Any" 

1185 elif (flags & 0x80) != 0: 

1186 subtable.TextDirection = "Vertical" 

1187 else: 

1188 subtable.TextDirection = "Horizontal" 

1189 

1190 def read(self, reader, font, tableDict): 

1191 pos = reader.pos 

1192 m = MorxSubtable() 

1193 m.StructLength = reader.readULong() 

1194 flags = reader.readUInt8() 

1195 orderKey = ((flags & 0x40) != 0, (flags & 0x10) != 0) 

1196 m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey] 

1197 self._setTextDirectionFromCoverageFlags(flags, m) 

1198 m.Reserved = reader.readUShort() 

1199 m.Reserved |= (flags & 0xF) << 16 

1200 m.MorphType = reader.readUInt8() 

1201 m.SubFeatureFlags = reader.readULong() 

1202 tableClass = lookupTypes["morx"].get(m.MorphType) 

1203 if tableClass is None: 

1204 assert False, "unsupported 'morx' lookup type %s" % m.MorphType 

1205 # To decode AAT ligatures, we need to know the subtable size. 

1206 # The easiest way to pass this along is to create a new reader 

1207 # that works on just the subtable as its data. 

1208 headerLength = reader.pos - pos 

1209 data = reader.data[reader.pos : reader.pos + m.StructLength - headerLength] 

1210 assert len(data) == m.StructLength - headerLength 

1211 subReader = OTTableReader(data=data, tableTag=reader.tableTag) 

1212 m.SubStruct = tableClass() 

1213 m.SubStruct.decompile(subReader, font) 

1214 reader.seek(pos + m.StructLength) 

1215 return m 

1216 

1217 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1218 xmlWriter.begintag(name, attrs) 

1219 xmlWriter.newline() 

1220 xmlWriter.comment("StructLength=%d" % value.StructLength) 

1221 xmlWriter.newline() 

1222 xmlWriter.simpletag("TextDirection", value=value.TextDirection) 

1223 xmlWriter.newline() 

1224 xmlWriter.simpletag("ProcessingOrder", value=value.ProcessingOrder) 

1225 xmlWriter.newline() 

1226 if value.Reserved != 0: 

1227 xmlWriter.simpletag("Reserved", value="0x%04x" % value.Reserved) 

1228 xmlWriter.newline() 

1229 xmlWriter.comment("MorphType=%d" % value.MorphType) 

1230 xmlWriter.newline() 

1231 xmlWriter.simpletag("SubFeatureFlags", value="0x%08x" % value.SubFeatureFlags) 

1232 xmlWriter.newline() 

1233 value.SubStruct.toXML(xmlWriter, font) 

1234 xmlWriter.endtag(name) 

1235 xmlWriter.newline() 

1236 

1237 def xmlRead(self, attrs, content, font): 

1238 m = MorxSubtable() 

1239 covFlags = 0 

1240 m.Reserved = 0 

1241 for eltName, eltAttrs, eltContent in filter(istuple, content): 

1242 if eltName == "CoverageFlags": 

1243 # Only in XML from old versions of fonttools. 

1244 covFlags = safeEval(eltAttrs["value"]) 

1245 orderKey = ((covFlags & 0x40) != 0, (covFlags & 0x10) != 0) 

1246 m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey] 

1247 self._setTextDirectionFromCoverageFlags(covFlags, m) 

1248 elif eltName == "ProcessingOrder": 

1249 m.ProcessingOrder = eltAttrs["value"] 

1250 assert m.ProcessingOrder in self._PROCESSING_ORDERS_REVERSED, ( 

1251 "unknown ProcessingOrder: %s" % m.ProcessingOrder 

1252 ) 

1253 elif eltName == "TextDirection": 

1254 m.TextDirection = eltAttrs["value"] 

1255 assert m.TextDirection in {"Horizontal", "Vertical", "Any"}, ( 

1256 "unknown TextDirection %s" % m.TextDirection 

1257 ) 

1258 elif eltName == "Reserved": 

1259 m.Reserved = safeEval(eltAttrs["value"]) 

1260 elif eltName == "SubFeatureFlags": 

1261 m.SubFeatureFlags = safeEval(eltAttrs["value"]) 

1262 elif eltName.endswith("Morph"): 

1263 m.fromXML(eltName, eltAttrs, eltContent, font) 

1264 else: 

1265 assert False, eltName 

1266 m.Reserved = (covFlags & 0xF) << 16 | m.Reserved 

1267 return m 

1268 

1269 def write(self, writer, font, tableDict, value, repeatIndex=None): 

1270 covFlags = (value.Reserved & 0x000F0000) >> 16 

1271 reverseOrder, logicalOrder = self._PROCESSING_ORDERS_REVERSED[ 

1272 value.ProcessingOrder 

1273 ] 

1274 covFlags |= 0x80 if value.TextDirection == "Vertical" else 0 

1275 covFlags |= 0x40 if reverseOrder else 0 

1276 covFlags |= 0x20 if value.TextDirection == "Any" else 0 

1277 covFlags |= 0x10 if logicalOrder else 0 

1278 value.CoverageFlags = covFlags 

1279 lengthIndex = len(writer.items) 

1280 before = writer.getDataLength() 

1281 value.StructLength = 0xDEADBEEF 

1282 # The high nibble of value.Reserved is actuallly encoded 

1283 # into coverageFlags, so we need to clear it here. 

1284 origReserved = value.Reserved # including high nibble 

1285 value.Reserved = value.Reserved & 0xFFFF # without high nibble 

1286 value.compile(writer, font) 

1287 value.Reserved = origReserved # restore original value 

1288 assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef" 

1289 length = writer.getDataLength() - before 

1290 writer.items[lengthIndex] = struct.pack(">L", length) 

1291 

1292 

1293# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader 

1294# TODO: Untangle the implementation of the various lookup-specific formats. 

1295class STXHeader(BaseConverter): 

1296 def __init__(self, name, repeat, aux, tableClass, *, description=""): 

1297 BaseConverter.__init__( 

1298 self, name, repeat, aux, tableClass, description=description 

1299 ) 

1300 assert issubclass(self.tableClass, AATAction) 

1301 self.classLookup = AATLookup("GlyphClasses", None, None, UShort) 

1302 if issubclass(self.tableClass, ContextualMorphAction): 

1303 self.perGlyphLookup = AATLookup("PerGlyphLookup", None, None, GlyphID) 

1304 else: 

1305 self.perGlyphLookup = None 

1306 

1307 def read(self, reader, font, tableDict): 

1308 table = AATStateTable() 

1309 pos = reader.pos 

1310 classTableReader = reader.getSubReader(0) 

1311 stateArrayReader = reader.getSubReader(0) 

1312 entryTableReader = reader.getSubReader(0) 

1313 actionReader = None 

1314 ligaturesReader = None 

1315 table.GlyphClassCount = reader.readULong() 

1316 classTableReader.seek(pos + reader.readULong()) 

1317 stateArrayReader.seek(pos + reader.readULong()) 

1318 entryTableReader.seek(pos + reader.readULong()) 

1319 if self.perGlyphLookup is not None: 

1320 perGlyphTableReader = reader.getSubReader(0) 

1321 perGlyphTableReader.seek(pos + reader.readULong()) 

1322 if issubclass(self.tableClass, LigatureMorphAction): 

1323 actionReader = reader.getSubReader(0) 

1324 actionReader.seek(pos + reader.readULong()) 

1325 ligComponentReader = reader.getSubReader(0) 

1326 ligComponentReader.seek(pos + reader.readULong()) 

1327 ligaturesReader = reader.getSubReader(0) 

1328 ligaturesReader.seek(pos + reader.readULong()) 

1329 numLigComponents = (ligaturesReader.pos - ligComponentReader.pos) // 2 

1330 assert numLigComponents >= 0 

1331 table.LigComponents = ligComponentReader.readUShortArray(numLigComponents) 

1332 table.Ligatures = self._readLigatures(ligaturesReader, font) 

1333 elif issubclass(self.tableClass, InsertionMorphAction): 

1334 actionReader = reader.getSubReader(0) 

1335 actionReader.seek(pos + reader.readULong()) 

1336 table.GlyphClasses = self.classLookup.read(classTableReader, font, tableDict) 

1337 numStates = int( 

1338 (entryTableReader.pos - stateArrayReader.pos) / (table.GlyphClassCount * 2) 

1339 ) 

1340 for stateIndex in range(numStates): 

1341 state = AATState() 

1342 table.States.append(state) 

1343 for glyphClass in range(table.GlyphClassCount): 

1344 entryIndex = stateArrayReader.readUShort() 

1345 state.Transitions[glyphClass] = self._readTransition( 

1346 entryTableReader, entryIndex, font, actionReader 

1347 ) 

1348 if self.perGlyphLookup is not None: 

1349 table.PerGlyphLookups = self._readPerGlyphLookups( 

1350 table, perGlyphTableReader, font 

1351 ) 

1352 return table 

1353 

1354 def _readTransition(self, reader, entryIndex, font, actionReader): 

1355 transition = self.tableClass() 

1356 entryReader = reader.getSubReader( 

1357 reader.pos + entryIndex * transition.staticSize 

1358 ) 

1359 transition.decompile(entryReader, font, actionReader) 

1360 return transition 

1361 

1362 def _readLigatures(self, reader, font): 

1363 limit = len(reader.data) 

1364 numLigatureGlyphs = (limit - reader.pos) // 2 

1365 return font.getGlyphNameMany(reader.readUShortArray(numLigatureGlyphs)) 

1366 

1367 def _countPerGlyphLookups(self, table): 

1368 # Somewhat annoyingly, the morx table does not encode 

1369 # the size of the per-glyph table. So we need to find 

1370 # the maximum value that MorphActions use as index 

1371 # into this table. 

1372 numLookups = 0 

1373 for state in table.States: 

1374 for t in state.Transitions.values(): 

1375 if isinstance(t, ContextualMorphAction): 

1376 if t.MarkIndex != 0xFFFF: 

1377 numLookups = max(numLookups, t.MarkIndex + 1) 

1378 if t.CurrentIndex != 0xFFFF: 

1379 numLookups = max(numLookups, t.CurrentIndex + 1) 

1380 return numLookups 

1381 

1382 def _readPerGlyphLookups(self, table, reader, font): 

1383 pos = reader.pos 

1384 lookups = [] 

1385 for _ in range(self._countPerGlyphLookups(table)): 

1386 lookupReader = reader.getSubReader(0) 

1387 lookupReader.seek(pos + reader.readULong()) 

1388 lookups.append(self.perGlyphLookup.read(lookupReader, font, {})) 

1389 return lookups 

1390 

1391 def write(self, writer, font, tableDict, value, repeatIndex=None): 

1392 glyphClassWriter = OTTableWriter() 

1393 self.classLookup.write( 

1394 glyphClassWriter, font, tableDict, value.GlyphClasses, repeatIndex=None 

1395 ) 

1396 glyphClassData = pad(glyphClassWriter.getAllData(), 2) 

1397 glyphClassCount = max(value.GlyphClasses.values()) + 1 

1398 glyphClassTableOffset = 16 # size of STXHeader 

1399 if self.perGlyphLookup is not None: 

1400 glyphClassTableOffset += 4 

1401 

1402 glyphClassTableOffset += self.tableClass.actionHeaderSize 

1403 actionData, actionIndex = self.tableClass.compileActions(font, value.States) 

1404 stateArrayData, entryTableData = self._compileStates( 

1405 font, value.States, glyphClassCount, actionIndex 

1406 ) 

1407 stateArrayOffset = glyphClassTableOffset + len(glyphClassData) 

1408 entryTableOffset = stateArrayOffset + len(stateArrayData) 

1409 perGlyphOffset = entryTableOffset + len(entryTableData) 

1410 perGlyphData = pad(self._compilePerGlyphLookups(value, font), 4) 

1411 if actionData is not None: 

1412 actionOffset = entryTableOffset + len(entryTableData) 

1413 else: 

1414 actionOffset = None 

1415 

1416 ligaturesOffset, ligComponentsOffset = None, None 

1417 ligComponentsData = self._compileLigComponents(value, font) 

1418 ligaturesData = self._compileLigatures(value, font) 

1419 if ligComponentsData is not None: 

1420 assert len(perGlyphData) == 0 

1421 ligComponentsOffset = actionOffset + len(actionData) 

1422 ligaturesOffset = ligComponentsOffset + len(ligComponentsData) 

1423 

1424 writer.writeULong(glyphClassCount) 

1425 writer.writeULong(glyphClassTableOffset) 

1426 writer.writeULong(stateArrayOffset) 

1427 writer.writeULong(entryTableOffset) 

1428 if self.perGlyphLookup is not None: 

1429 writer.writeULong(perGlyphOffset) 

1430 if actionOffset is not None: 

1431 writer.writeULong(actionOffset) 

1432 if ligComponentsOffset is not None: 

1433 writer.writeULong(ligComponentsOffset) 

1434 writer.writeULong(ligaturesOffset) 

1435 writer.writeData(glyphClassData) 

1436 writer.writeData(stateArrayData) 

1437 writer.writeData(entryTableData) 

1438 writer.writeData(perGlyphData) 

1439 if actionData is not None: 

1440 writer.writeData(actionData) 

1441 if ligComponentsData is not None: 

1442 writer.writeData(ligComponentsData) 

1443 if ligaturesData is not None: 

1444 writer.writeData(ligaturesData) 

1445 

1446 def _compileStates(self, font, states, glyphClassCount, actionIndex): 

1447 stateArrayWriter = OTTableWriter() 

1448 entries, entryIDs = [], {} 

1449 for state in states: 

1450 for glyphClass in range(glyphClassCount): 

1451 transition = state.Transitions[glyphClass] 

1452 entryWriter = OTTableWriter() 

1453 transition.compile(entryWriter, font, actionIndex) 

1454 entryData = entryWriter.getAllData() 

1455 assert ( 

1456 len(entryData) == transition.staticSize 

1457 ), "%s has staticSize %d, " "but actually wrote %d bytes" % ( 

1458 repr(transition), 

1459 transition.staticSize, 

1460 len(entryData), 

1461 ) 

1462 entryIndex = entryIDs.get(entryData) 

1463 if entryIndex is None: 

1464 entryIndex = len(entries) 

1465 entryIDs[entryData] = entryIndex 

1466 entries.append(entryData) 

1467 stateArrayWriter.writeUShort(entryIndex) 

1468 stateArrayData = pad(stateArrayWriter.getAllData(), 4) 

1469 entryTableData = pad(bytesjoin(entries), 4) 

1470 return stateArrayData, entryTableData 

1471 

1472 def _compilePerGlyphLookups(self, table, font): 

1473 if self.perGlyphLookup is None: 

1474 return b"" 

1475 numLookups = self._countPerGlyphLookups(table) 

1476 assert len(table.PerGlyphLookups) == numLookups, ( 

1477 "len(AATStateTable.PerGlyphLookups) is %d, " 

1478 "but the actions inside the table refer to %d" 

1479 % (len(table.PerGlyphLookups), numLookups) 

1480 ) 

1481 writer = OTTableWriter() 

1482 for lookup in table.PerGlyphLookups: 

1483 lookupWriter = writer.getSubWriter() 

1484 self.perGlyphLookup.write(lookupWriter, font, {}, lookup, None) 

1485 writer.writeSubTable(lookupWriter, offsetSize=4) 

1486 return writer.getAllData() 

1487 

1488 def _compileLigComponents(self, table, font): 

1489 if not hasattr(table, "LigComponents"): 

1490 return None 

1491 writer = OTTableWriter() 

1492 for component in table.LigComponents: 

1493 writer.writeUShort(component) 

1494 return writer.getAllData() 

1495 

1496 def _compileLigatures(self, table, font): 

1497 if not hasattr(table, "Ligatures"): 

1498 return None 

1499 writer = OTTableWriter() 

1500 for glyphName in table.Ligatures: 

1501 writer.writeUShort(font.getGlyphID(glyphName)) 

1502 return writer.getAllData() 

1503 

1504 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1505 xmlWriter.begintag(name, attrs) 

1506 xmlWriter.newline() 

1507 xmlWriter.comment("GlyphClassCount=%s" % value.GlyphClassCount) 

1508 xmlWriter.newline() 

1509 for g, klass in sorted(value.GlyphClasses.items()): 

1510 xmlWriter.simpletag("GlyphClass", glyph=g, value=klass) 

1511 xmlWriter.newline() 

1512 for stateIndex, state in enumerate(value.States): 

1513 xmlWriter.begintag("State", index=stateIndex) 

1514 xmlWriter.newline() 

1515 for glyphClass, trans in sorted(state.Transitions.items()): 

1516 trans.toXML( 

1517 xmlWriter, 

1518 font=font, 

1519 attrs={"onGlyphClass": glyphClass}, 

1520 name="Transition", 

1521 ) 

1522 xmlWriter.endtag("State") 

1523 xmlWriter.newline() 

1524 for i, lookup in enumerate(value.PerGlyphLookups): 

1525 xmlWriter.begintag("PerGlyphLookup", index=i) 

1526 xmlWriter.newline() 

1527 for glyph, val in sorted(lookup.items()): 

1528 xmlWriter.simpletag("Lookup", glyph=glyph, value=val) 

1529 xmlWriter.newline() 

1530 xmlWriter.endtag("PerGlyphLookup") 

1531 xmlWriter.newline() 

1532 if hasattr(value, "LigComponents"): 

1533 xmlWriter.begintag("LigComponents") 

1534 xmlWriter.newline() 

1535 for i, val in enumerate(getattr(value, "LigComponents")): 

1536 xmlWriter.simpletag("LigComponent", index=i, value=val) 

1537 xmlWriter.newline() 

1538 xmlWriter.endtag("LigComponents") 

1539 xmlWriter.newline() 

1540 self._xmlWriteLigatures(xmlWriter, font, value, name, attrs) 

1541 xmlWriter.endtag(name) 

1542 xmlWriter.newline() 

1543 

1544 def _xmlWriteLigatures(self, xmlWriter, font, value, name, attrs): 

1545 if not hasattr(value, "Ligatures"): 

1546 return 

1547 xmlWriter.begintag("Ligatures") 

1548 xmlWriter.newline() 

1549 for i, g in enumerate(getattr(value, "Ligatures")): 

1550 xmlWriter.simpletag("Ligature", index=i, glyph=g) 

1551 xmlWriter.newline() 

1552 xmlWriter.endtag("Ligatures") 

1553 xmlWriter.newline() 

1554 

1555 def xmlRead(self, attrs, content, font): 

1556 table = AATStateTable() 

1557 for eltName, eltAttrs, eltContent in filter(istuple, content): 

1558 if eltName == "GlyphClass": 

1559 glyph = eltAttrs["glyph"] 

1560 value = eltAttrs["value"] 

1561 table.GlyphClasses[glyph] = safeEval(value) 

1562 elif eltName == "State": 

1563 state = self._xmlReadState(eltAttrs, eltContent, font) 

1564 table.States.append(state) 

1565 elif eltName == "PerGlyphLookup": 

1566 lookup = self.perGlyphLookup.xmlRead(eltAttrs, eltContent, font) 

1567 table.PerGlyphLookups.append(lookup) 

1568 elif eltName == "LigComponents": 

1569 table.LigComponents = self._xmlReadLigComponents( 

1570 eltAttrs, eltContent, font 

1571 ) 

1572 elif eltName == "Ligatures": 

1573 table.Ligatures = self._xmlReadLigatures(eltAttrs, eltContent, font) 

1574 table.GlyphClassCount = max(table.GlyphClasses.values()) + 1 

1575 return table 

1576 

1577 def _xmlReadState(self, attrs, content, font): 

1578 state = AATState() 

1579 for eltName, eltAttrs, eltContent in filter(istuple, content): 

1580 if eltName == "Transition": 

1581 glyphClass = safeEval(eltAttrs["onGlyphClass"]) 

1582 transition = self.tableClass() 

1583 transition.fromXML(eltName, eltAttrs, eltContent, font) 

1584 state.Transitions[glyphClass] = transition 

1585 return state 

1586 

1587 def _xmlReadLigComponents(self, attrs, content, font): 

1588 ligComponents = [] 

1589 for eltName, eltAttrs, _eltContent in filter(istuple, content): 

1590 if eltName == "LigComponent": 

1591 ligComponents.append(safeEval(eltAttrs["value"])) 

1592 return ligComponents 

1593 

1594 def _xmlReadLigatures(self, attrs, content, font): 

1595 ligs = [] 

1596 for eltName, eltAttrs, _eltContent in filter(istuple, content): 

1597 if eltName == "Ligature": 

1598 ligs.append(eltAttrs["glyph"]) 

1599 return ligs 

1600 

1601 

1602class CIDGlyphMap(BaseConverter): 

1603 def read(self, reader, font, tableDict): 

1604 numCIDs = reader.readUShort() 

1605 result = {} 

1606 for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)): 

1607 if glyphID != 0xFFFF: 

1608 result[cid] = font.getGlyphName(glyphID) 

1609 return result 

1610 

1611 def write(self, writer, font, tableDict, value, repeatIndex=None): 

1612 items = {cid: font.getGlyphID(glyph) for cid, glyph in value.items()} 

1613 count = max(items) + 1 if items else 0 

1614 writer.writeUShort(count) 

1615 for cid in range(count): 

1616 writer.writeUShort(items.get(cid, 0xFFFF)) 

1617 

1618 def xmlRead(self, attrs, content, font): 

1619 result = {} 

1620 for eName, eAttrs, _eContent in filter(istuple, content): 

1621 if eName == "CID": 

1622 result[safeEval(eAttrs["cid"])] = eAttrs["glyph"].strip() 

1623 return result 

1624 

1625 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1626 xmlWriter.begintag(name, attrs) 

1627 xmlWriter.newline() 

1628 for cid, glyph in sorted(value.items()): 

1629 if glyph is not None and glyph != 0xFFFF: 

1630 xmlWriter.simpletag("CID", cid=cid, glyph=glyph) 

1631 xmlWriter.newline() 

1632 xmlWriter.endtag(name) 

1633 xmlWriter.newline() 

1634 

1635 

1636class GlyphCIDMap(BaseConverter): 

1637 def read(self, reader, font, tableDict): 

1638 glyphOrder = font.getGlyphOrder() 

1639 count = reader.readUShort() 

1640 cids = reader.readUShortArray(count) 

1641 if count > len(glyphOrder): 

1642 log.warning( 

1643 "GlyphCIDMap has %d elements, " 

1644 "but the font has only %d glyphs; " 

1645 "ignoring the rest" % (count, len(glyphOrder)) 

1646 ) 

1647 result = {} 

1648 for glyphID in range(min(len(cids), len(glyphOrder))): 

1649 cid = cids[glyphID] 

1650 if cid != 0xFFFF: 

1651 result[glyphOrder[glyphID]] = cid 

1652 return result 

1653 

1654 def write(self, writer, font, tableDict, value, repeatIndex=None): 

1655 items = { 

1656 font.getGlyphID(g): cid 

1657 for g, cid in value.items() 

1658 if cid is not None and cid != 0xFFFF 

1659 } 

1660 count = max(items) + 1 if items else 0 

1661 writer.writeUShort(count) 

1662 for glyphID in range(count): 

1663 writer.writeUShort(items.get(glyphID, 0xFFFF)) 

1664 

1665 def xmlRead(self, attrs, content, font): 

1666 result = {} 

1667 for eName, eAttrs, _eContent in filter(istuple, content): 

1668 if eName == "CID": 

1669 result[eAttrs["glyph"]] = safeEval(eAttrs["value"]) 

1670 return result 

1671 

1672 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1673 xmlWriter.begintag(name, attrs) 

1674 xmlWriter.newline() 

1675 for glyph, cid in sorted(value.items()): 

1676 if cid is not None and cid != 0xFFFF: 

1677 xmlWriter.simpletag("CID", glyph=glyph, value=cid) 

1678 xmlWriter.newline() 

1679 xmlWriter.endtag(name) 

1680 xmlWriter.newline() 

1681 

1682 

1683class DeltaValue(BaseConverter): 

1684 def read(self, reader, font, tableDict): 

1685 StartSize = tableDict["StartSize"] 

1686 EndSize = tableDict["EndSize"] 

1687 DeltaFormat = tableDict["DeltaFormat"] 

1688 assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat" 

1689 nItems = EndSize - StartSize + 1 

1690 nBits = 1 << DeltaFormat 

1691 minusOffset = 1 << nBits 

1692 mask = (1 << nBits) - 1 

1693 signMask = 1 << (nBits - 1) 

1694 

1695 DeltaValue = [] 

1696 tmp, shift = 0, 0 

1697 for i in range(nItems): 

1698 if shift == 0: 

1699 tmp, shift = reader.readUShort(), 16 

1700 shift = shift - nBits 

1701 value = (tmp >> shift) & mask 

1702 if value & signMask: 

1703 value = value - minusOffset 

1704 DeltaValue.append(value) 

1705 return DeltaValue 

1706 

1707 def write(self, writer, font, tableDict, value, repeatIndex=None): 

1708 StartSize = tableDict["StartSize"] 

1709 EndSize = tableDict["EndSize"] 

1710 DeltaFormat = tableDict["DeltaFormat"] 

1711 DeltaValue = value 

1712 assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat" 

1713 nItems = EndSize - StartSize + 1 

1714 nBits = 1 << DeltaFormat 

1715 assert len(DeltaValue) == nItems 

1716 mask = (1 << nBits) - 1 

1717 

1718 tmp, shift = 0, 16 

1719 for value in DeltaValue: 

1720 shift = shift - nBits 

1721 tmp = tmp | ((value & mask) << shift) 

1722 if shift == 0: 

1723 writer.writeUShort(tmp) 

1724 tmp, shift = 0, 16 

1725 if shift != 16: 

1726 writer.writeUShort(tmp) 

1727 

1728 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1729 xmlWriter.simpletag(name, attrs + [("value", value)]) 

1730 xmlWriter.newline() 

1731 

1732 def xmlRead(self, attrs, content, font): 

1733 return safeEval(attrs["value"]) 

1734 

1735 

1736class VarIdxMapValue(BaseConverter): 

1737 def read(self, reader, font, tableDict): 

1738 fmt = tableDict["EntryFormat"] 

1739 nItems = tableDict["MappingCount"] 

1740 

1741 innerBits = 1 + (fmt & 0x000F) 

1742 innerMask = (1 << innerBits) - 1 

1743 outerMask = 0xFFFFFFFF - innerMask 

1744 outerShift = 16 - innerBits 

1745 

1746 entrySize = 1 + ((fmt & 0x0030) >> 4) 

1747 readArray = { 

1748 1: reader.readUInt8Array, 

1749 2: reader.readUShortArray, 

1750 3: reader.readUInt24Array, 

1751 4: reader.readULongArray, 

1752 }[entrySize] 

1753 

1754 return [ 

1755 (((raw & outerMask) << outerShift) | (raw & innerMask)) 

1756 for raw in readArray(nItems) 

1757 ] 

1758 

1759 def write(self, writer, font, tableDict, value, repeatIndex=None): 

1760 fmt = tableDict["EntryFormat"] 

1761 mapping = value 

1762 writer["MappingCount"].setValue(len(mapping)) 

1763 

1764 innerBits = 1 + (fmt & 0x000F) 

1765 innerMask = (1 << innerBits) - 1 

1766 outerShift = 16 - innerBits 

1767 

1768 entrySize = 1 + ((fmt & 0x0030) >> 4) 

1769 writeArray = { 

1770 1: writer.writeUInt8Array, 

1771 2: writer.writeUShortArray, 

1772 3: writer.writeUInt24Array, 

1773 4: writer.writeULongArray, 

1774 }[entrySize] 

1775 

1776 writeArray( 

1777 [ 

1778 (((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask)) 

1779 for idx in mapping 

1780 ] 

1781 ) 

1782 

1783 

1784class VarDataValue(BaseConverter): 

1785 def read(self, reader, font, tableDict): 

1786 values = [] 

1787 

1788 regionCount = tableDict["VarRegionCount"] 

1789 wordCount = tableDict["NumShorts"] 

1790 

1791 # https://github.com/fonttools/fonttools/issues/2279 

1792 longWords = bool(wordCount & 0x8000) 

1793 wordCount = wordCount & 0x7FFF 

1794 

1795 if longWords: 

1796 readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray 

1797 else: 

1798 readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array 

1799 

1800 n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount) 

1801 values.extend(readBigArray(n1)) 

1802 values.extend(readSmallArray(n2 - n1)) 

1803 if n2 > regionCount: # Padding 

1804 del values[regionCount:] 

1805 

1806 return values 

1807 

1808 def write(self, writer, font, tableDict, values, repeatIndex=None): 

1809 regionCount = tableDict["VarRegionCount"] 

1810 wordCount = tableDict["NumShorts"] 

1811 

1812 # https://github.com/fonttools/fonttools/issues/2279 

1813 longWords = bool(wordCount & 0x8000) 

1814 wordCount = wordCount & 0x7FFF 

1815 

1816 (writeBigArray, writeSmallArray) = { 

1817 False: (writer.writeShortArray, writer.writeInt8Array), 

1818 True: (writer.writeLongArray, writer.writeShortArray), 

1819 }[longWords] 

1820 

1821 n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount) 

1822 writeBigArray(values[:n1]) 

1823 writeSmallArray(values[n1:regionCount]) 

1824 if n2 > regionCount: # Padding 

1825 writer.writeSmallArray([0] * (n2 - regionCount)) 

1826 

1827 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1828 xmlWriter.simpletag(name, attrs + [("value", value)]) 

1829 xmlWriter.newline() 

1830 

1831 def xmlRead(self, attrs, content, font): 

1832 return safeEval(attrs["value"]) 

1833 

1834 

1835class LookupFlag(UShort): 

1836 def xmlWrite(self, xmlWriter, font, value, name, attrs): 

1837 xmlWriter.simpletag(name, attrs + [("value", value)]) 

1838 flags = [] 

1839 if value & 0x01: 

1840 flags.append("rightToLeft") 

1841 if value & 0x02: 

1842 flags.append("ignoreBaseGlyphs") 

1843 if value & 0x04: 

1844 flags.append("ignoreLigatures") 

1845 if value & 0x08: 

1846 flags.append("ignoreMarks") 

1847 if value & 0x10: 

1848 flags.append("useMarkFilteringSet") 

1849 if value & 0xFF00: 

1850 flags.append("markAttachmentType[%i]" % (value >> 8)) 

1851 if flags: 

1852 xmlWriter.comment(" ".join(flags)) 

1853 xmlWriter.newline() 

1854 

1855 

1856class _UInt8Enum(UInt8): 

1857 enumClass = NotImplemented 

1858 

1859 def read(self, reader, font, tableDict): 

1860 return self.enumClass(super().read(reader, font, tableDict)) 

1861 

1862 @classmethod 

1863 def fromString(cls, value): 

1864 return getattr(cls.enumClass, value.upper()) 

1865 

1866 @classmethod 

1867 def toString(cls, value): 

1868 return cls.enumClass(value).name.lower() 

1869 

1870 

1871class ExtendMode(_UInt8Enum): 

1872 enumClass = _ExtendMode 

1873 

1874 

1875class CompositeMode(_UInt8Enum): 

1876 enumClass = _CompositeMode 

1877 

1878 

1879converterMapping = { 

1880 # type class 

1881 "int8": Int8, 

1882 "int16": Short, 

1883 "uint8": UInt8, 

1884 "uint16": UShort, 

1885 "uint24": UInt24, 

1886 "uint32": ULong, 

1887 "char64": Char64, 

1888 "Flags32": Flags32, 

1889 "VarIndex": VarIndex, 

1890 "Version": Version, 

1891 "Tag": Tag, 

1892 "GlyphID": GlyphID, 

1893 "GlyphID32": GlyphID32, 

1894 "NameID": NameID, 

1895 "DeciPoints": DeciPoints, 

1896 "Fixed": Fixed, 

1897 "F2Dot14": F2Dot14, 

1898 "Angle": Angle, 

1899 "BiasedAngle": BiasedAngle, 

1900 "struct": Struct, 

1901 "Offset": Table, 

1902 "LOffset": LTable, 

1903 "Offset24": Table24, 

1904 "ValueRecord": ValueRecord, 

1905 "DeltaValue": DeltaValue, 

1906 "VarIdxMapValue": VarIdxMapValue, 

1907 "VarDataValue": VarDataValue, 

1908 "LookupFlag": LookupFlag, 

1909 "ExtendMode": ExtendMode, 

1910 "CompositeMode": CompositeMode, 

1911 "STATFlags": STATFlags, 

1912 # AAT 

1913 "CIDGlyphMap": CIDGlyphMap, 

1914 "GlyphCIDMap": GlyphCIDMap, 

1915 "MortChain": StructWithLength, 

1916 "MortSubtable": StructWithLength, 

1917 "MorxChain": StructWithLength, 

1918 "MorxSubtable": MorxSubtableConverter, 

1919 # "Template" types 

1920 "AATLookup": lambda C: partial(AATLookup, tableClass=C), 

1921 "AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C), 

1922 "STXHeader": lambda C: partial(STXHeader, tableClass=C), 

1923 "OffsetTo": lambda C: partial(Table, tableClass=C), 

1924 "LOffsetTo": lambda C: partial(LTable, tableClass=C), 

1925 "LOffset24To": lambda C: partial(Table24, tableClass=C), 

1926}