-
Notifications
You must be signed in to change notification settings - Fork 448
/
otTables.py
2306 lines (2025 loc) · 81.5 KB
/
otTables.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# coding: utf-8
"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various
OpenType subtables.
Most are constructed upon import from data in otData.py, all are populated with
converter objects from otConverters.py.
"""
import copy
from enum import IntEnum
from functools import reduce
from math import radians
import itertools
from collections import defaultdict, namedtuple
from fontTools.ttLib.tables.otTraverse import dfs_base_table
from fontTools.misc.arrayTools import quantizeRect
from fontTools.misc.roundTools import otRound
from fontTools.misc.transform import Transform, Identity
from fontTools.misc.textTools import bytesjoin, pad, safeEval
from fontTools.pens.boundsPen import ControlBoundsPen
from fontTools.pens.transformPen import TransformPen
from .otBase import (
BaseTable,
FormatSwitchingBaseTable,
ValueRecord,
CountReference,
getFormatSwitchingBaseTableClass,
)
from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
import logging
import struct
from typing import TYPE_CHECKING, Iterator, List, Optional, Set
if TYPE_CHECKING:
from fontTools.ttLib.ttGlyphSet import _TTGlyphSet
log = logging.getLogger(__name__)
class AATStateTable(object):
def __init__(self):
self.GlyphClasses = {} # GlyphID --> GlyphClass
self.States = [] # List of AATState, indexed by state number
self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...]
class AATState(object):
def __init__(self):
self.Transitions = {} # GlyphClass --> AATAction
class AATAction(object):
_FLAGS = None
@staticmethod
def compileActions(font, states):
return (None, None)
def _writeFlagsToXML(self, xmlWriter):
flags = [f for f in self._FLAGS if self.__dict__[f]]
if flags:
xmlWriter.simpletag("Flags", value=",".join(flags))
xmlWriter.newline()
if self.ReservedFlags != 0:
xmlWriter.simpletag("ReservedFlags", value="0x%04X" % self.ReservedFlags)
xmlWriter.newline()
def _setFlag(self, flag):
assert flag in self._FLAGS, "unsupported flag %s" % flag
self.__dict__[flag] = True
class RearrangementMorphAction(AATAction):
staticSize = 4
actionHeaderSize = 0
_FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"]
_VERBS = {
0: "no change",
1: "Ax ⇒ xA",
2: "xD ⇒ Dx",
3: "AxD ⇒ DxA",
4: "ABx ⇒ xAB",
5: "ABx ⇒ xBA",
6: "xCD ⇒ CDx",
7: "xCD ⇒ DCx",
8: "AxCD ⇒ CDxA",
9: "AxCD ⇒ DCxA",
10: "ABxD ⇒ DxAB",
11: "ABxD ⇒ DxBA",
12: "ABxCD ⇒ CDxAB",
13: "ABxCD ⇒ CDxBA",
14: "ABxCD ⇒ DCxAB",
15: "ABxCD ⇒ DCxBA",
}
def __init__(self):
self.NewState = 0
self.Verb = 0
self.MarkFirst = False
self.DontAdvance = False
self.MarkLast = False
self.ReservedFlags = 0
def compile(self, writer, font, actionIndex):
assert actionIndex is None
writer.writeUShort(self.NewState)
assert self.Verb >= 0 and self.Verb <= 15, self.Verb
flags = self.Verb | self.ReservedFlags
if self.MarkFirst:
flags |= 0x8000
if self.DontAdvance:
flags |= 0x4000
if self.MarkLast:
flags |= 0x2000
writer.writeUShort(flags)
def decompile(self, reader, font, actionReader):
assert actionReader is None
self.NewState = reader.readUShort()
flags = reader.readUShort()
self.Verb = flags & 0xF
self.MarkFirst = bool(flags & 0x8000)
self.DontAdvance = bool(flags & 0x4000)
self.MarkLast = bool(flags & 0x2000)
self.ReservedFlags = flags & 0x1FF0
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
xmlWriter.newline()
xmlWriter.simpletag("NewState", value=self.NewState)
xmlWriter.newline()
self._writeFlagsToXML(xmlWriter)
xmlWriter.simpletag("Verb", value=self.Verb)
verbComment = self._VERBS.get(self.Verb)
if verbComment is not None:
xmlWriter.comment(verbComment)
xmlWriter.newline()
xmlWriter.endtag(name)
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
self.NewState = self.Verb = self.ReservedFlags = 0
self.MarkFirst = self.DontAdvance = self.MarkLast = False
content = [t for t in content if isinstance(t, tuple)]
for eltName, eltAttrs, eltContent in content:
if eltName == "NewState":
self.NewState = safeEval(eltAttrs["value"])
elif eltName == "Verb":
self.Verb = safeEval(eltAttrs["value"])
elif eltName == "ReservedFlags":
self.ReservedFlags = safeEval(eltAttrs["value"])
elif eltName == "Flags":
for flag in eltAttrs["value"].split(","):
self._setFlag(flag.strip())
class ContextualMorphAction(AATAction):
staticSize = 8
actionHeaderSize = 0
_FLAGS = ["SetMark", "DontAdvance"]
def __init__(self):
self.NewState = 0
self.SetMark, self.DontAdvance = False, False
self.ReservedFlags = 0
self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
def compile(self, writer, font, actionIndex):
assert actionIndex is None
writer.writeUShort(self.NewState)
flags = self.ReservedFlags
if self.SetMark:
flags |= 0x8000
if self.DontAdvance:
flags |= 0x4000
writer.writeUShort(flags)
writer.writeUShort(self.MarkIndex)
writer.writeUShort(self.CurrentIndex)
def decompile(self, reader, font, actionReader):
assert actionReader is None
self.NewState = reader.readUShort()
flags = reader.readUShort()
self.SetMark = bool(flags & 0x8000)
self.DontAdvance = bool(flags & 0x4000)
self.ReservedFlags = flags & 0x3FFF
self.MarkIndex = reader.readUShort()
self.CurrentIndex = reader.readUShort()
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
xmlWriter.newline()
xmlWriter.simpletag("NewState", value=self.NewState)
xmlWriter.newline()
self._writeFlagsToXML(xmlWriter)
xmlWriter.simpletag("MarkIndex", value=self.MarkIndex)
xmlWriter.newline()
xmlWriter.simpletag("CurrentIndex", value=self.CurrentIndex)
xmlWriter.newline()
xmlWriter.endtag(name)
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
self.NewState = self.ReservedFlags = 0
self.SetMark = self.DontAdvance = False
self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
content = [t for t in content if isinstance(t, tuple)]
for eltName, eltAttrs, eltContent in content:
if eltName == "NewState":
self.NewState = safeEval(eltAttrs["value"])
elif eltName == "Flags":
for flag in eltAttrs["value"].split(","):
self._setFlag(flag.strip())
elif eltName == "ReservedFlags":
self.ReservedFlags = safeEval(eltAttrs["value"])
elif eltName == "MarkIndex":
self.MarkIndex = safeEval(eltAttrs["value"])
elif eltName == "CurrentIndex":
self.CurrentIndex = safeEval(eltAttrs["value"])
class LigAction(object):
def __init__(self):
self.Store = False
# GlyphIndexDelta is a (possibly negative) delta that gets
# added to the glyph ID at the top of the AAT runtime
# execution stack. It is *not* a byte offset into the
# morx table. The result of the addition, which is performed
# at run time by the shaping engine, is an index into
# the ligature components table. See 'morx' specification.
# In the AAT specification, this field is called Offset;
# but its meaning is quite different from other offsets
# in either AAT or OpenType, so we use a different name.
self.GlyphIndexDelta = 0
class LigatureMorphAction(AATAction):
staticSize = 6
# 4 bytes for each of {action,ligComponents,ligatures}Offset
actionHeaderSize = 12
_FLAGS = ["SetComponent", "DontAdvance"]
def __init__(self):
self.NewState = 0
self.SetComponent, self.DontAdvance = False, False
self.ReservedFlags = 0
self.Actions = []
def compile(self, writer, font, actionIndex):
assert actionIndex is not None
writer.writeUShort(self.NewState)
flags = self.ReservedFlags
if self.SetComponent:
flags |= 0x8000
if self.DontAdvance:
flags |= 0x4000
if len(self.Actions) > 0:
flags |= 0x2000
writer.writeUShort(flags)
if len(self.Actions) > 0:
actions = self.compileLigActions()
writer.writeUShort(actionIndex[actions])
else:
writer.writeUShort(0)
def decompile(self, reader, font, actionReader):
assert actionReader is not None
self.NewState = reader.readUShort()
flags = reader.readUShort()
self.SetComponent = bool(flags & 0x8000)
self.DontAdvance = bool(flags & 0x4000)
performAction = bool(flags & 0x2000)
# As of 2017-09-12, the 'morx' specification says that
# the reserved bitmask in ligature subtables is 0x3FFF.
# However, the specification also defines a flag 0x2000,
# so the reserved value should actually be 0x1FFF.
# TODO: Report this specification bug to Apple.
self.ReservedFlags = flags & 0x1FFF
actionIndex = reader.readUShort()
if performAction:
self.Actions = self._decompileLigActions(actionReader, actionIndex)
else:
self.Actions = []
@staticmethod
def compileActions(font, states):
result, actions, actionIndex = b"", set(), {}
for state in states:
for _glyphClass, trans in state.Transitions.items():
actions.add(trans.compileLigActions())
# Sort the compiled actions in decreasing order of
# length, so that the longer sequence come before the
# shorter ones. For each compiled action ABCD, its
# suffixes BCD, CD, and D do not be encoded separately
# (in case they occur); instead, we can just store an
# index that points into the middle of the longer
# sequence. Every compiled AAT ligature sequence is
# terminated with an end-of-sequence flag, which can
# only be set on the last element of the sequence.
# Therefore, it is sufficient to consider just the
# suffixes.
for a in sorted(actions, key=lambda x: (-len(x), x)):
if a not in actionIndex:
for i in range(0, len(a), 4):
suffix = a[i:]
suffixIndex = (len(result) + i) // 4
actionIndex.setdefault(suffix, suffixIndex)
result += a
result = pad(result, 4)
return (result, actionIndex)
def compileLigActions(self):
result = []
for i, action in enumerate(self.Actions):
last = i == len(self.Actions) - 1
value = action.GlyphIndexDelta & 0x3FFFFFFF
value |= 0x80000000 if last else 0
value |= 0x40000000 if action.Store else 0
result.append(struct.pack(">L", value))
return bytesjoin(result)
def _decompileLigActions(self, actionReader, actionIndex):
actions = []
last = False
reader = actionReader.getSubReader(actionReader.pos + actionIndex * 4)
while not last:
value = reader.readULong()
last = bool(value & 0x80000000)
action = LigAction()
actions.append(action)
action.Store = bool(value & 0x40000000)
delta = value & 0x3FFFFFFF
if delta >= 0x20000000: # sign-extend 30-bit value
delta = -0x40000000 + delta
action.GlyphIndexDelta = delta
return actions
def fromXML(self, name, attrs, content, font):
self.NewState = self.ReservedFlags = 0
self.SetComponent = self.DontAdvance = False
self.ReservedFlags = 0
self.Actions = []
content = [t for t in content if isinstance(t, tuple)]
for eltName, eltAttrs, eltContent in content:
if eltName == "NewState":
self.NewState = safeEval(eltAttrs["value"])
elif eltName == "Flags":
for flag in eltAttrs["value"].split(","):
self._setFlag(flag.strip())
elif eltName == "ReservedFlags":
self.ReservedFlags = safeEval(eltAttrs["value"])
elif eltName == "Action":
action = LigAction()
flags = eltAttrs.get("Flags", "").split(",")
flags = [f.strip() for f in flags]
action.Store = "Store" in flags
action.GlyphIndexDelta = safeEval(eltAttrs["GlyphIndexDelta"])
self.Actions.append(action)
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
xmlWriter.newline()
xmlWriter.simpletag("NewState", value=self.NewState)
xmlWriter.newline()
self._writeFlagsToXML(xmlWriter)
for action in self.Actions:
attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)]
if action.Store:
attribs.append(("Flags", "Store"))
xmlWriter.simpletag("Action", attribs)
xmlWriter.newline()
xmlWriter.endtag(name)
xmlWriter.newline()
class InsertionMorphAction(AATAction):
staticSize = 8
actionHeaderSize = 4 # 4 bytes for actionOffset
_FLAGS = [
"SetMark",
"DontAdvance",
"CurrentIsKashidaLike",
"MarkedIsKashidaLike",
"CurrentInsertBefore",
"MarkedInsertBefore",
]
def __init__(self):
self.NewState = 0
for flag in self._FLAGS:
setattr(self, flag, False)
self.ReservedFlags = 0
self.CurrentInsertionAction, self.MarkedInsertionAction = [], []
def compile(self, writer, font, actionIndex):
assert actionIndex is not None
writer.writeUShort(self.NewState)
flags = self.ReservedFlags
if self.SetMark:
flags |= 0x8000
if self.DontAdvance:
flags |= 0x4000
if self.CurrentIsKashidaLike:
flags |= 0x2000
if self.MarkedIsKashidaLike:
flags |= 0x1000
if self.CurrentInsertBefore:
flags |= 0x0800
if self.MarkedInsertBefore:
flags |= 0x0400
flags |= len(self.CurrentInsertionAction) << 5
flags |= len(self.MarkedInsertionAction)
writer.writeUShort(flags)
if len(self.CurrentInsertionAction) > 0:
currentIndex = actionIndex[tuple(self.CurrentInsertionAction)]
else:
currentIndex = 0xFFFF
writer.writeUShort(currentIndex)
if len(self.MarkedInsertionAction) > 0:
markedIndex = actionIndex[tuple(self.MarkedInsertionAction)]
else:
markedIndex = 0xFFFF
writer.writeUShort(markedIndex)
def decompile(self, reader, font, actionReader):
assert actionReader is not None
self.NewState = reader.readUShort()
flags = reader.readUShort()
self.SetMark = bool(flags & 0x8000)
self.DontAdvance = bool(flags & 0x4000)
self.CurrentIsKashidaLike = bool(flags & 0x2000)
self.MarkedIsKashidaLike = bool(flags & 0x1000)
self.CurrentInsertBefore = bool(flags & 0x0800)
self.MarkedInsertBefore = bool(flags & 0x0400)
self.CurrentInsertionAction = self._decompileInsertionAction(
actionReader, font, index=reader.readUShort(), count=((flags & 0x03E0) >> 5)
)
self.MarkedInsertionAction = self._decompileInsertionAction(
actionReader, font, index=reader.readUShort(), count=(flags & 0x001F)
)
def _decompileInsertionAction(self, actionReader, font, index, count):
if index == 0xFFFF or count == 0:
return []
reader = actionReader.getSubReader(actionReader.pos + index * 2)
return font.getGlyphNameMany(reader.readUShortArray(count))
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
xmlWriter.newline()
xmlWriter.simpletag("NewState", value=self.NewState)
xmlWriter.newline()
self._writeFlagsToXML(xmlWriter)
for g in self.CurrentInsertionAction:
xmlWriter.simpletag("CurrentInsertionAction", glyph=g)
xmlWriter.newline()
for g in self.MarkedInsertionAction:
xmlWriter.simpletag("MarkedInsertionAction", glyph=g)
xmlWriter.newline()
xmlWriter.endtag(name)
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
self.__init__()
content = [t for t in content if isinstance(t, tuple)]
for eltName, eltAttrs, eltContent in content:
if eltName == "NewState":
self.NewState = safeEval(eltAttrs["value"])
elif eltName == "Flags":
for flag in eltAttrs["value"].split(","):
self._setFlag(flag.strip())
elif eltName == "CurrentInsertionAction":
self.CurrentInsertionAction.append(eltAttrs["glyph"])
elif eltName == "MarkedInsertionAction":
self.MarkedInsertionAction.append(eltAttrs["glyph"])
else:
assert False, eltName
@staticmethod
def compileActions(font, states):
actions, actionIndex, result = set(), {}, b""
for state in states:
for _glyphClass, trans in state.Transitions.items():
if trans.CurrentInsertionAction is not None:
actions.add(tuple(trans.CurrentInsertionAction))
if trans.MarkedInsertionAction is not None:
actions.add(tuple(trans.MarkedInsertionAction))
# Sort the compiled actions in decreasing order of
# length, so that the longer sequence come before the
# shorter ones.
for action in sorted(actions, key=lambda x: (-len(x), x)):
# We insert all sub-sequences of the action glyph sequence
# into actionIndex. For example, if one action triggers on
# glyph sequence [A, B, C, D, E] and another action triggers
# on [C, D], we return result=[A, B, C, D, E] (as list of
# encoded glyph IDs), and actionIndex={('A','B','C','D','E'): 0,
# ('C','D'): 2}.
if action in actionIndex:
continue
for start in range(0, len(action)):
startIndex = (len(result) // 2) + start
for limit in range(start, len(action)):
glyphs = action[start : limit + 1]
actionIndex.setdefault(glyphs, startIndex)
for glyph in action:
glyphID = font.getGlyphID(glyph)
result += struct.pack(">H", glyphID)
return result, actionIndex
class FeatureParams(BaseTable):
def compile(self, writer, font):
assert (
featureParamTypes.get(writer["FeatureTag"]) == self.__class__
), "Wrong FeatureParams type for feature '%s': %s" % (
writer["FeatureTag"],
self.__class__.__name__,
)
BaseTable.compile(self, writer, font)
def toXML(self, xmlWriter, font, attrs=None, name=None):
BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__)
class FeatureParamsSize(FeatureParams):
pass
class FeatureParamsStylisticSet(FeatureParams):
pass
class FeatureParamsCharacterVariants(FeatureParams):
pass
class Coverage(FormatSwitchingBaseTable):
# manual implementation to get rid of glyphID dependencies
def populateDefaults(self, propagator=None):
if not hasattr(self, "glyphs"):
self.glyphs = []
def postRead(self, rawTable, font):
if self.Format == 1:
self.glyphs = rawTable["GlyphArray"]
elif self.Format == 2:
glyphs = self.glyphs = []
ranges = rawTable["RangeRecord"]
# Some SIL fonts have coverage entries that don't have sorted
# StartCoverageIndex. If it is so, fixup and warn. We undo
# this when writing font out.
sorted_ranges = sorted(ranges, key=lambda a: a.StartCoverageIndex)
if ranges != sorted_ranges:
log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
ranges = sorted_ranges
del sorted_ranges
for r in ranges:
start = r.Start
end = r.End
startID = font.getGlyphID(start)
endID = font.getGlyphID(end) + 1
glyphs.extend(font.getGlyphNameMany(range(startID, endID)))
else:
self.glyphs = []
log.warning("Unknown Coverage format: %s", self.Format)
del self.Format # Don't need this anymore
def preWrite(self, font):
glyphs = getattr(self, "glyphs", None)
if glyphs is None:
glyphs = self.glyphs = []
format = 1
rawTable = {"GlyphArray": glyphs}
if glyphs:
# find out whether Format 2 is more compact or not
glyphIDs = font.getGlyphIDMany(glyphs)
brokenOrder = sorted(glyphIDs) != glyphIDs
last = glyphIDs[0]
ranges = [[last]]
for glyphID in glyphIDs[1:]:
if glyphID != last + 1:
ranges[-1].append(last)
ranges.append([glyphID])
last = glyphID
ranges[-1].append(last)
if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word
# Format 2 is more compact
index = 0
for i in range(len(ranges)):
start, end = ranges[i]
r = RangeRecord()
r.StartID = start
r.Start = font.getGlyphName(start)
r.End = font.getGlyphName(end)
r.StartCoverageIndex = index
ranges[i] = r
index = index + end - start + 1
if brokenOrder:
log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
ranges.sort(key=lambda a: a.StartID)
for r in ranges:
del r.StartID
format = 2
rawTable = {"RangeRecord": ranges}
# else:
# fallthrough; Format 1 is more compact
self.Format = format
return rawTable
def toXML2(self, xmlWriter, font):
for glyphName in getattr(self, "glyphs", []):
xmlWriter.simpletag("Glyph", value=glyphName)
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
glyphs = getattr(self, "glyphs", None)
if glyphs is None:
glyphs = []
self.glyphs = glyphs
glyphs.append(attrs["value"])
# The special 0xFFFFFFFF delta-set index is used to indicate that there
# is no variation data in the ItemVariationStore for a given variable field
NO_VARIATION_INDEX = 0xFFFFFFFF
class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
def populateDefaults(self, propagator=None):
if not hasattr(self, "mapping"):
self.mapping = []
def postRead(self, rawTable, font):
assert (rawTable["EntryFormat"] & 0xFFC0) == 0
self.mapping = rawTable["mapping"]
@staticmethod
def getEntryFormat(mapping):
ored = 0
for idx in mapping:
ored |= idx
inner = ored & 0xFFFF
innerBits = 0
while inner:
innerBits += 1
inner >>= 1
innerBits = max(innerBits, 1)
assert innerBits <= 16
ored = (ored >> (16 - innerBits)) | (ored & ((1 << innerBits) - 1))
if ored <= 0x000000FF:
entrySize = 1
elif ored <= 0x0000FFFF:
entrySize = 2
elif ored <= 0x00FFFFFF:
entrySize = 3
else:
entrySize = 4
return ((entrySize - 1) << 4) | (innerBits - 1)
def preWrite(self, font):
mapping = getattr(self, "mapping", None)
if mapping is None:
mapping = self.mapping = []
self.Format = 1 if len(mapping) > 0xFFFF else 0
rawTable = self.__dict__.copy()
rawTable["MappingCount"] = len(mapping)
rawTable["EntryFormat"] = self.getEntryFormat(mapping)
return rawTable
def toXML2(self, xmlWriter, font):
# Make xml dump less verbose, by omitting no-op entries like:
# <Map index="..." outer="65535" inner="65535"/>
xmlWriter.comment("Omitted values default to 0xFFFF/0xFFFF (no variations)")
xmlWriter.newline()
for i, value in enumerate(getattr(self, "mapping", [])):
attrs = [("index", i)]
if value != NO_VARIATION_INDEX:
attrs.extend(
[
("outer", value >> 16),
("inner", value & 0xFFFF),
]
)
xmlWriter.simpletag("Map", attrs)
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
mapping = getattr(self, "mapping", None)
if mapping is None:
self.mapping = mapping = []
index = safeEval(attrs["index"])
outer = safeEval(attrs.get("outer", "0xFFFF"))
inner = safeEval(attrs.get("inner", "0xFFFF"))
assert inner <= 0xFFFF
mapping.insert(index, (outer << 16) | inner)
def __getitem__(self, i):
return self.mapping[i] if i < len(self.mapping) else NO_VARIATION_INDEX
class VarIdxMap(BaseTable):
def populateDefaults(self, propagator=None):
if not hasattr(self, "mapping"):
self.mapping = {}
def postRead(self, rawTable, font):
assert (rawTable["EntryFormat"] & 0xFFC0) == 0
glyphOrder = font.getGlyphOrder()
mapList = rawTable["mapping"]
mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
self.mapping = dict(zip(glyphOrder, mapList))
def preWrite(self, font):
mapping = getattr(self, "mapping", None)
if mapping is None:
mapping = self.mapping = {}
glyphOrder = font.getGlyphOrder()
mapping = [mapping[g] for g in glyphOrder]
while len(mapping) > 1 and mapping[-2] == mapping[-1]:
del mapping[-1]
rawTable = {"mapping": mapping}
rawTable["MappingCount"] = len(mapping)
rawTable["EntryFormat"] = DeltaSetIndexMap.getEntryFormat(mapping)
return rawTable
def toXML2(self, xmlWriter, font):
for glyph, value in sorted(getattr(self, "mapping", {}).items()):
attrs = (
("glyph", glyph),
("outer", value >> 16),
("inner", value & 0xFFFF),
)
xmlWriter.simpletag("Map", attrs)
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
mapping = getattr(self, "mapping", None)
if mapping is None:
mapping = {}
self.mapping = mapping
try:
glyph = attrs["glyph"]
except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836
glyph = font.getGlyphOrder()[attrs["index"]]
outer = safeEval(attrs["outer"])
inner = safeEval(attrs["inner"])
assert inner <= 0xFFFF
mapping[glyph] = (outer << 16) | inner
def __getitem__(self, glyphName):
return self.mapping.get(glyphName, NO_VARIATION_INDEX)
class VarRegionList(BaseTable):
def preWrite(self, font):
# The OT spec says VarStore.VarRegionList.RegionAxisCount should always
# be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule
# even when the VarRegionList is empty. We can't treat RegionAxisCount
# like a normal propagated count (== len(Region[i].VarRegionAxis)),
# otherwise it would default to 0 if VarRegionList is empty.
# Thus, we force it to always be equal to fvar.axisCount.
# https://github.com/khaledhosny/ots/pull/192
fvarTable = font.get("fvar")
if fvarTable:
self.RegionAxisCount = len(fvarTable.axes)
return {
**self.__dict__,
"RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount"),
}
class SingleSubst(FormatSwitchingBaseTable):
def populateDefaults(self, propagator=None):
if not hasattr(self, "mapping"):
self.mapping = {}
def postRead(self, rawTable, font):
mapping = {}
input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
if self.Format == 1:
delta = rawTable["DeltaGlyphID"]
inputGIDS = font.getGlyphIDMany(input)
outGIDS = [(glyphID + delta) % 65536 for glyphID in inputGIDS]
outNames = font.getGlyphNameMany(outGIDS)
for inp, out in zip(input, outNames):
mapping[inp] = out
elif self.Format == 2:
assert (
len(input) == rawTable["GlyphCount"]
), "invalid SingleSubstFormat2 table"
subst = rawTable["Substitute"]
for inp, sub in zip(input, subst):
mapping[inp] = sub
else:
assert 0, "unknown format: %s" % self.Format
self.mapping = mapping
del self.Format # Don't need this anymore
def preWrite(self, font):
mapping = getattr(self, "mapping", None)
if mapping is None:
mapping = self.mapping = {}
items = list(mapping.items())
getGlyphID = font.getGlyphID
gidItems = [(getGlyphID(a), getGlyphID(b)) for a, b in items]
sortableItems = sorted(zip(gidItems, items))
# figure out format
format = 2
delta = None
for inID, outID in gidItems:
if delta is None:
delta = (outID - inID) % 65536
if (inID + delta) % 65536 != outID:
break
else:
if delta is None:
# the mapping is empty, better use format 2
format = 2
else:
format = 1
rawTable = {}
self.Format = format
cov = Coverage()
input = [item[1][0] for item in sortableItems]
subst = [item[1][1] for item in sortableItems]
cov.glyphs = input
rawTable["Coverage"] = cov
if format == 1:
assert delta is not None
rawTable["DeltaGlyphID"] = delta
else:
rawTable["Substitute"] = subst
return rawTable
def toXML2(self, xmlWriter, font):
items = sorted(self.mapping.items())
for inGlyph, outGlyph in items:
xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)])
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
mapping = getattr(self, "mapping", None)
if mapping is None:
mapping = {}
self.mapping = mapping
mapping[attrs["in"]] = attrs["out"]
class MultipleSubst(FormatSwitchingBaseTable):
def populateDefaults(self, propagator=None):
if not hasattr(self, "mapping"):
self.mapping = {}
def postRead(self, rawTable, font):
mapping = {}
if self.Format == 1:
glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"])
subst = [s.Substitute for s in rawTable["Sequence"]]
mapping = dict(zip(glyphs, subst))
else:
assert 0, "unknown format: %s" % self.Format
self.mapping = mapping
del self.Format # Don't need this anymore
def preWrite(self, font):
mapping = getattr(self, "mapping", None)
if mapping is None:
mapping = self.mapping = {}
cov = Coverage()
cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID)
self.Format = 1
rawTable = {
"Coverage": cov,
"Sequence": [self.makeSequence_(mapping[glyph]) for glyph in cov.glyphs],
}
return rawTable
def toXML2(self, xmlWriter, font):
items = sorted(self.mapping.items())
for inGlyph, outGlyphs in items:
out = ",".join(outGlyphs)
xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", out)])
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
mapping = getattr(self, "mapping", None)
if mapping is None:
mapping = {}
self.mapping = mapping
# TTX v3.0 and earlier.
if name == "Coverage":
self.old_coverage_ = []
for element in content:
if not isinstance(element, tuple):
continue
element_name, element_attrs, _ = element
if element_name == "Glyph":
self.old_coverage_.append(element_attrs["value"])
return
if name == "Sequence":
index = int(attrs.get("index", len(mapping)))
glyph = self.old_coverage_[index]
glyph_mapping = mapping[glyph] = []
for element in content:
if not isinstance(element, tuple):
continue
element_name, element_attrs, _ = element
if element_name == "Substitute":
glyph_mapping.append(element_attrs["value"])
return
# TTX v3.1 and later.
outGlyphs = attrs["out"].split(",") if attrs["out"] else []
mapping[attrs["in"]] = [g.strip() for g in outGlyphs]
@staticmethod
def makeSequence_(g):
seq = Sequence()
seq.Substitute = g
return seq
class ClassDef(FormatSwitchingBaseTable):
def populateDefaults(self, propagator=None):
if not hasattr(self, "classDefs"):
self.classDefs = {}
def postRead(self, rawTable, font):
classDefs = {}
if self.Format == 1:
start = rawTable["StartGlyph"]
classList = rawTable["ClassValueArray"]
startID = font.getGlyphID(start)
endID = startID + len(classList)
glyphNames = font.getGlyphNameMany(range(startID, endID))
for glyphName, cls in zip(glyphNames, classList):
if cls:
classDefs[glyphName] = cls
elif self.Format == 2:
records = rawTable["ClassRangeRecord"]
for rec in records:
cls = rec.Class
if not cls:
continue
start = rec.Start
end = rec.End
startID = font.getGlyphID(start)
endID = font.getGlyphID(end) + 1
glyphNames = font.getGlyphNameMany(range(startID, endID))
for glyphName in glyphNames:
classDefs[glyphName] = cls
else:
log.warning("Unknown ClassDef format: %s", self.Format)
self.classDefs = classDefs
del self.Format # Don't need this anymore
def _getClassRanges(self, font):
classDefs = getattr(self, "classDefs", None)
if classDefs is None:
self.classDefs = {}
return
getGlyphID = font.getGlyphID
items = []
for glyphName, cls in classDefs.items():
if not cls:
continue
items.append((getGlyphID(glyphName), glyphName, cls))
if items:
items.sort()
last, lastName, lastCls = items[0]
ranges = [[lastCls, last, lastName]]
for glyphID, glyphName, cls in items[1:]:
if glyphID != last + 1 or cls != lastCls:
ranges[-1].extend([last, lastName])
ranges.append([cls, glyphID, glyphName])
last = glyphID
lastName = glyphName
lastCls = cls
ranges[-1].extend([last, lastName])
return ranges
def preWrite(self, font):
format = 2