1
1
import time
2
+ from defcon import Anchor
2
3
from fontTools .pens .boundsPen import ControlBoundsPen
3
4
from fontTools .pens .hashPointPen import HashPointPen
4
5
from fontTools .ttLib import TTFont , TTLibError
@@ -43,6 +44,7 @@ def extractFontFromOpenType(
43
44
doFeatures = True ,
44
45
customFunctions = [],
45
46
doInstructions = True ,
47
+ doAnchors = True ,
46
48
):
47
49
source = TTFont (pathOrFile )
48
50
if doInfo :
@@ -64,6 +66,8 @@ def extractFontFromOpenType(
64
66
function (source , destination )
65
67
if doInstructions :
66
68
extractInstructions (source , destination )
69
+ if doAnchors :
70
+ extractAnchors (source , destination )
67
71
source .close ()
68
72
69
73
@@ -595,7 +599,7 @@ def _extractOpenTypeKerningFromGPOS(source):
595
599
kerningDictionaries ,
596
600
leftClassDictionaries ,
597
601
rightClassDictionaries ,
598
- ) = _gatherDataFromLookups (gpos , scriptOrder )
602
+ ) = _gatherKerningDataFromLookups (gpos , scriptOrder )
599
603
# merge all kerning pairs
600
604
kerning = _mergeKerningDictionaries (kerningDictionaries )
601
605
# get rid of groups that have only one member
@@ -647,12 +651,12 @@ def _makeScriptOrder(gpos):
647
651
return sorted (scripts )
648
652
649
653
650
- def _gatherDataFromLookups (gpos , scriptOrder ):
654
+ def _gatherKerningDataFromLookups (gpos , scriptOrder ):
651
655
"""
652
656
Gather kerning and classes from the applicable lookups
653
657
and return them in script order.
654
658
"""
655
- lookupIndexes = _gatherLookupIndexes (gpos )
659
+ lookupIndexes = _gatherLookupIndexes (gpos , [ "kern" ] )
656
660
seenLookups = set ()
657
661
kerningDictionaries = []
658
662
leftClassDictionaries = []
@@ -679,50 +683,50 @@ def _gatherDataFromLookups(gpos, scriptOrder):
679
683
return kerningDictionaries , leftClassDictionaries , rightClassDictionaries
680
684
681
685
682
- def _gatherLookupIndexes (gpos ):
686
+ def _gatherLookupIndexes (gpos , featureTags ):
683
687
"""
684
688
Gather a mapping of script to lookup indexes
685
- referenced by the kern feature for each script.
689
+ referenced by the desired features for each script.
686
690
Returns a dictionary of this structure:
687
691
{
688
692
"latn" : [0],
689
693
"DFLT" : [0]
690
694
}
691
695
"""
692
- # gather the indexes of the kern features
693
- kernFeatureIndexes = [
696
+ # gather the indexes of the desired features
697
+ desiredFeatureIndexes = [
694
698
index
695
699
for index , featureRecord in enumerate (gpos .FeatureList .FeatureRecord )
696
- if featureRecord .FeatureTag == "kern"
700
+ if featureRecord .FeatureTag in featureTags
697
701
]
698
- # find scripts and languages that have kern features
699
- scriptKernFeatureIndexes = {}
702
+ # find scripts and languages that have desired features
703
+ scriptDesiredFeatureIndexes = {}
700
704
for scriptRecord in gpos .ScriptList .ScriptRecord :
701
705
script = scriptRecord .ScriptTag
702
- thisScriptKernFeatureIndexes = []
706
+ thisScriptDesiredFeatureIndexes = []
703
707
defaultLangSysRecord = scriptRecord .Script .DefaultLangSys
704
708
if defaultLangSysRecord is not None :
705
709
f = []
706
710
for featureIndex in defaultLangSysRecord .FeatureIndex :
707
- if featureIndex not in kernFeatureIndexes :
711
+ if featureIndex not in desiredFeatureIndexes :
708
712
continue
709
713
f .append (featureIndex )
710
714
if f :
711
- thisScriptKernFeatureIndexes .append ((None , f ))
715
+ thisScriptDesiredFeatureIndexes .append ((None , f ))
712
716
if scriptRecord .Script .LangSysRecord is not None :
713
717
for langSysRecord in scriptRecord .Script .LangSysRecord :
714
718
langSys = langSysRecord .LangSysTag
715
719
f = []
716
720
for featureIndex in langSysRecord .LangSys .FeatureIndex :
717
- if featureIndex not in kernFeatureIndexes :
721
+ if featureIndex not in desiredFeatureIndexes :
718
722
continue
719
723
f .append (featureIndex )
720
724
if f :
721
- thisScriptKernFeatureIndexes .append ((langSys , f ))
722
- scriptKernFeatureIndexes [script ] = thisScriptKernFeatureIndexes
725
+ thisScriptDesiredFeatureIndexes .append ((langSys , f ))
726
+ scriptDesiredFeatureIndexes [script ] = thisScriptDesiredFeatureIndexes
723
727
# convert the feature indexes to lookup indexes
724
728
scriptLookupIndexes = {}
725
- for script , featureDefinitions in scriptKernFeatureIndexes .items ():
729
+ for script , featureDefinitions in scriptDesiredFeatureIndexes .items ():
726
730
lookupIndexes = scriptLookupIndexes [script ] = []
727
731
for language , featureIndexes in featureDefinitions :
728
732
for featureIndex in featureIndexes :
@@ -1078,3 +1082,110 @@ def extractOpenTypeFeatures(source):
1078
1082
if _haveFontFeatures :
1079
1083
return unparse (source ).asFea ()
1080
1084
return ""
1085
+
1086
+
1087
+ # -------
1088
+ # Anchors
1089
+ # -------
1090
+
1091
+
1092
+ def extractAnchors (source , destination ):
1093
+ gpos = source ["GPOS" ].table
1094
+ # get an ordered list of scripts
1095
+ scriptOrder = _makeScriptOrder (gpos )
1096
+ # extract anchors from each applicable lookup
1097
+ anchorGroups = _gatherAnchorDataFromLookups (gpos , scriptOrder )
1098
+ #print(anchorGroups)
1099
+
1100
+ for groupIndex , groupAnchors in enumerate (anchorGroups ):
1101
+ baseAnchors = groupAnchors ["baseAnchors" ]
1102
+ markAnchors = groupAnchors ["markAnchors" ]
1103
+
1104
+ for base in baseAnchors .keys ():
1105
+ try :
1106
+ anchor = Anchor (destination [base ], {"x" : baseAnchors [base ]["x" ], "y" : baseAnchors [base ]["y" ], "name" : f"Anchor-{ groupIndex } " })
1107
+ destination [base ].appendAnchor (anchor )
1108
+ except :
1109
+ continue
1110
+ for mark in markAnchors .keys ():
1111
+ try :
1112
+ anchor = Anchor (destination [mark ], {"x" : markAnchors [mark ]["x" ], "y" : markAnchors [mark ]["y" ], "name" : f"_Anchor-{ groupIndex } " })
1113
+ destination [mark ].appendAnchor (anchor )
1114
+ except :
1115
+ continue
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