@@ -46,6 +46,7 @@ def extractFontFromOpenType(
46
46
doFeatures = True ,
47
47
customFunctions = [],
48
48
doInstructions = True ,
49
+ doAnchors = True ,
49
50
):
50
51
source = TTFont (pathOrFile )
51
52
if doInfo :
@@ -67,6 +68,8 @@ def extractFontFromOpenType(
67
68
function (source , destination )
68
69
if doInstructions :
69
70
extractInstructions (source , destination )
71
+ if doAnchors :
72
+ extractAnchors (source , destination )
70
73
source .close ()
71
74
72
75
@@ -602,7 +605,7 @@ def _extractOpenTypeKerningFromGPOS(source):
602
605
kerningDictionaries ,
603
606
leftClassDictionaries ,
604
607
rightClassDictionaries ,
605
- ) = _gatherDataFromLookups (gpos , scriptOrder )
608
+ ) = _gatherKerningDataFromLookups (gpos , scriptOrder )
606
609
# merge all kerning pairs
607
610
kerning = _mergeKerningDictionaries (kerningDictionaries )
608
611
# get rid of groups that have only one member
@@ -654,12 +657,12 @@ def _makeScriptOrder(gpos):
654
657
return sorted (scripts )
655
658
656
659
657
- def _gatherDataFromLookups (gpos , scriptOrder ):
660
+ def _gatherKerningDataFromLookups (gpos , scriptOrder ):
658
661
"""
659
662
Gather kerning and classes from the applicable lookups
660
663
and return them in script order.
661
664
"""
662
- lookupIndexes = _gatherLookupIndexes (gpos )
665
+ lookupIndexes = _gatherLookupIndexes (gpos , [ "kern" ] )
663
666
seenLookups = set ()
664
667
kerningDictionaries = []
665
668
leftClassDictionaries = []
@@ -686,50 +689,50 @@ def _gatherDataFromLookups(gpos, scriptOrder):
686
689
return kerningDictionaries , leftClassDictionaries , rightClassDictionaries
687
690
688
691
689
- def _gatherLookupIndexes (gpos ):
692
+ def _gatherLookupIndexes (gpos , featureTags ):
690
693
"""
691
694
Gather a mapping of script to lookup indexes
692
- referenced by the kern feature for each script.
695
+ referenced by the desired features for each script.
693
696
Returns a dictionary of this structure:
694
697
{
695
698
"latn" : [0],
696
699
"DFLT" : [0]
697
700
}
698
701
"""
699
- # gather the indexes of the kern features
700
- kernFeatureIndexes = [
702
+ # gather the indexes of the desired features
703
+ desiredFeatureIndexes = [
701
704
index
702
705
for index , featureRecord in enumerate (gpos .FeatureList .FeatureRecord )
703
- if featureRecord .FeatureTag == "kern"
706
+ if featureRecord .FeatureTag in featureTags
704
707
]
705
- # find scripts and languages that have kern features
706
- scriptKernFeatureIndexes = {}
708
+ # find scripts and languages that have desired features
709
+ scriptDesiredFeatureIndexes = {}
707
710
for scriptRecord in gpos .ScriptList .ScriptRecord :
708
711
script = scriptRecord .ScriptTag
709
- thisScriptKernFeatureIndexes = []
712
+ thisScriptDesiredFeatureIndexes = []
710
713
defaultLangSysRecord = scriptRecord .Script .DefaultLangSys
711
714
if defaultLangSysRecord is not None :
712
715
f = []
713
716
for featureIndex in defaultLangSysRecord .FeatureIndex :
714
- if featureIndex not in kernFeatureIndexes :
717
+ if featureIndex not in desiredFeatureIndexes :
715
718
continue
716
719
f .append (featureIndex )
717
720
if f :
718
- thisScriptKernFeatureIndexes .append ((None , f ))
721
+ thisScriptDesiredFeatureIndexes .append ((None , f ))
719
722
if scriptRecord .Script .LangSysRecord is not None :
720
723
for langSysRecord in scriptRecord .Script .LangSysRecord :
721
724
langSys = langSysRecord .LangSysTag
722
725
f = []
723
726
for featureIndex in langSysRecord .LangSys .FeatureIndex :
724
- if featureIndex not in kernFeatureIndexes :
727
+ if featureIndex not in desiredFeatureIndexes :
725
728
continue
726
729
f .append (featureIndex )
727
730
if f :
728
- thisScriptKernFeatureIndexes .append ((langSys , f ))
729
- scriptKernFeatureIndexes [script ] = thisScriptKernFeatureIndexes
731
+ thisScriptDesiredFeatureIndexes .append ((langSys , f ))
732
+ scriptDesiredFeatureIndexes [script ] = thisScriptDesiredFeatureIndexes
730
733
# convert the feature indexes to lookup indexes
731
734
scriptLookupIndexes = {}
732
- for script , featureDefinitions in scriptKernFeatureIndexes .items ():
735
+ for script , featureDefinitions in scriptDesiredFeatureIndexes .items ():
733
736
lookupIndexes = scriptLookupIndexes [script ] = []
734
737
for language , featureIndexes in featureDefinitions :
735
738
for featureIndex in featureIndexes :
@@ -1085,3 +1088,104 @@ def extractOpenTypeFeatures(source):
1085
1088
if _haveFontFeatures :
1086
1089
return unparse (source ).asFea ()
1087
1090
return ""
1091
+
1092
+
1093
+ # -------
1094
+ # Anchors
1095
+ # -------
1096
+
1097
+
1098
+ def extractAnchors (source , destination ):
1099
+ if "GPOS" not in source :
1100
+ return
1101
+
1102
+ gpos = source ["GPOS" ].table
1103
+ # get an ordered list of scripts
1104
+ scriptOrder = _makeScriptOrder (gpos )
1105
+ # extract anchors from each applicable lookup
1106
+ anchorGroups = _gatherAnchorDataFromLookups (gpos , scriptOrder )
1107
+
1108
+ for groupIndex , groupAnchors in enumerate (anchorGroups ):
1109
+ baseAnchors = groupAnchors ["baseAnchors" ]
1110
+ markAnchors = groupAnchors ["markAnchors" ]
1111
+
1112
+ for base in baseAnchors .keys ():
1113
+ destination [base ].appendAnchor ({"x" : baseAnchors [base ]["x" ], "y" : baseAnchors [base ]["y" ], "name" : f"Anchor-{ groupIndex } " })
1114
+ for mark in markAnchors .keys ():
1115
+ destination [mark ].appendAnchor ({"x" : markAnchors [mark ]["x" ], "y" : markAnchors [mark ]["y" ], "name" : f"_Anchor-{ groupIndex } " })
1116
+
1117
+
1118
+ def _gatherAnchorDataFromLookups (gpos , scriptOrder ):
1119
+ """
1120
+ Gather anchor data from the applicable lookups
1121
+ and return them in script order.
1122
+ """
1123
+ lookupIndexes = _gatherLookupIndexes (gpos , ["mark" , "mkmk" ])
1124
+
1125
+ allAnchors = []
1126
+ seenLookups = set ()
1127
+ for script in scriptOrder :
1128
+ for lookupIndex in lookupIndexes [script ]:
1129
+ if lookupIndex in seenLookups :
1130
+ continue
1131
+ seenLookups .add (lookupIndex )
1132
+ anchorsForThisLookup = _gatherAnchorsForLookup (gpos , lookupIndex )
1133
+ allAnchors = allAnchors + anchorsForThisLookup
1134
+ return allAnchors
1135
+
1136
+
1137
+ def _gatherAnchorsForLookup (gpos , lookupIndex ):
1138
+ """
1139
+ Gather the anchor data for a particular lookup.
1140
+ Returns a list of anchor group data dicts in the following format:
1141
+ {
1142
+ "baseAnchors": {"A": {"x": 672, "y": 1600}, "B": {"x": 624, "y": 1600}},
1143
+ "markAnchors": {'gravecomb': {'x': -400, 'y': 1500}, 'acutecomb': {'x': -630, 'y': 1500}},
1144
+ }
1145
+ """
1146
+ allAnchorGroups = []
1147
+ lookup = gpos .LookupList .Lookup [lookupIndex ]
1148
+ # Type 4 are mark-to-base attachment lookups, type 6 are mark-to-mark ones, type 9 are extended lookups.
1149
+ if lookup .LookupType not in (4 , 6 , 9 ):
1150
+ return allAnchorGroups
1151
+ if lookup .LookupType == 9 and lookup .SubTable [0 ].ExtensionLookupType not in (4 ,6 ):
1152
+ return allAnchorGroups
1153
+ for subtableIndex , subtable in enumerate (lookup .SubTable ):
1154
+ if (subtable .Format != 1 ):
1155
+ print (f" Skipping Anchor lookup subtable of unknown format { subtable .Format } ." )
1156
+ continue
1157
+ if (lookup .LookupType == 9 ):
1158
+ subtable = subtable .ExtSubTable
1159
+ subtableAnchors = _handleAnchorLookupType4Format1 (subtable )
1160
+ allAnchorGroups .append (subtableAnchors )
1161
+ return allAnchorGroups
1162
+
1163
+
1164
+ def _handleAnchorLookupType4Format1 (subtable ):
1165
+ """
1166
+ Extract anchors from a Lookup Type 4 Format 1.
1167
+ """
1168
+ anchors = {
1169
+ "baseAnchors" : {},
1170
+ "markAnchors" : {},
1171
+ }
1172
+
1173
+ if subtable .LookupType not in (4 , 6 ):
1174
+ print (f" Skipping Anchor lookup subtable with unsupported LookupType { subtable .LookupType } ." )
1175
+ return anchors
1176
+
1177
+ subtableIsType4 = subtable .LookupType == 4
1178
+
1179
+ baseCoverage = subtable .BaseCoverage .glyphs if subtableIsType4 else subtable .Mark2Coverage .glyphs
1180
+ markCoverage = subtable .MarkCoverage .glyphs if subtableIsType4 else subtable .Mark1Coverage .glyphs
1181
+
1182
+ for baseRecordIndex , baseRecord in enumerate (subtable .BaseArray .BaseRecord if subtableIsType4 else subtable .Mark2Array .Mark2Record ):
1183
+ baseAnchor = baseRecord .BaseAnchor [0 ] if subtableIsType4 else baseRecord .Mark2Anchor [0 ]
1184
+ anchors ["baseAnchors" ].update ({baseCoverage [baseRecordIndex ]: {"x" : baseAnchor .XCoordinate , "y" : baseAnchor .YCoordinate }})
1185
+
1186
+ for markRecordIndex , markRecord in enumerate (subtable .MarkArray .MarkRecord if subtableIsType4 else subtable .Mark1Array .MarkRecord ):
1187
+ markAnchor = markRecord .MarkAnchor
1188
+ anchors ["markAnchors" ].update ({markCoverage [markRecordIndex ]: {"x" : markAnchor .XCoordinate , "y" : markAnchor .YCoordinate }})
1189
+
1190
+ return anchors
1191
+
0 commit comments