@@ -46,6 +46,7 @@ def extractFontFromOpenType(
4646 doFeatures = True ,
4747 customFunctions = [],
4848 doInstructions = True ,
49+ doAnchors = True ,
4950):
5051 source = TTFont (pathOrFile )
5152 if doInfo :
@@ -67,6 +68,8 @@ def extractFontFromOpenType(
6768 function (source , destination )
6869 if doInstructions :
6970 extractInstructions (source , destination )
71+ if doAnchors :
72+ extractAnchors (source , destination )
7073 source .close ()
7174
7275
@@ -602,7 +605,7 @@ def _extractOpenTypeKerningFromGPOS(source):
602605 kerningDictionaries ,
603606 leftClassDictionaries ,
604607 rightClassDictionaries ,
605- ) = _gatherDataFromLookups (gpos , scriptOrder )
608+ ) = _gatherKerningDataFromLookups (gpos , scriptOrder )
606609 # merge all kerning pairs
607610 kerning = _mergeKerningDictionaries (kerningDictionaries )
608611 # get rid of groups that have only one member
@@ -654,12 +657,12 @@ def _makeScriptOrder(gpos):
654657 return sorted (scripts )
655658
656659
657- def _gatherDataFromLookups (gpos , scriptOrder ):
660+ def _gatherKerningDataFromLookups (gpos , scriptOrder ):
658661 """
659662 Gather kerning and classes from the applicable lookups
660663 and return them in script order.
661664 """
662- lookupIndexes = _gatherLookupIndexes (gpos )
665+ lookupIndexes = _gatherLookupIndexes (gpos , [ "kern" ] )
663666 seenLookups = set ()
664667 kerningDictionaries = []
665668 leftClassDictionaries = []
@@ -686,50 +689,50 @@ def _gatherDataFromLookups(gpos, scriptOrder):
686689 return kerningDictionaries , leftClassDictionaries , rightClassDictionaries
687690
688691
689- def _gatherLookupIndexes (gpos ):
692+ def _gatherLookupIndexes (gpos , featureTags ):
690693 """
691694 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.
693696 Returns a dictionary of this structure:
694697 {
695698 "latn" : [0],
696699 "DFLT" : [0]
697700 }
698701 """
699- # gather the indexes of the kern features
700- kernFeatureIndexes = [
702+ # gather the indexes of the desired features
703+ desiredFeatureIndexes = [
701704 index
702705 for index , featureRecord in enumerate (gpos .FeatureList .FeatureRecord )
703- if featureRecord .FeatureTag == "kern"
706+ if featureRecord .FeatureTag in featureTags
704707 ]
705- # find scripts and languages that have kern features
706- scriptKernFeatureIndexes = {}
708+ # find scripts and languages that have desired features
709+ scriptDesiredFeatureIndexes = {}
707710 for scriptRecord in gpos .ScriptList .ScriptRecord :
708711 script = scriptRecord .ScriptTag
709- thisScriptKernFeatureIndexes = []
712+ thisScriptDesiredFeatureIndexes = []
710713 defaultLangSysRecord = scriptRecord .Script .DefaultLangSys
711714 if defaultLangSysRecord is not None :
712715 f = []
713716 for featureIndex in defaultLangSysRecord .FeatureIndex :
714- if featureIndex not in kernFeatureIndexes :
717+ if featureIndex not in desiredFeatureIndexes :
715718 continue
716719 f .append (featureIndex )
717720 if f :
718- thisScriptKernFeatureIndexes .append ((None , f ))
721+ thisScriptDesiredFeatureIndexes .append ((None , f ))
719722 if scriptRecord .Script .LangSysRecord is not None :
720723 for langSysRecord in scriptRecord .Script .LangSysRecord :
721724 langSys = langSysRecord .LangSysTag
722725 f = []
723726 for featureIndex in langSysRecord .LangSys .FeatureIndex :
724- if featureIndex not in kernFeatureIndexes :
727+ if featureIndex not in desiredFeatureIndexes :
725728 continue
726729 f .append (featureIndex )
727730 if f :
728- thisScriptKernFeatureIndexes .append ((langSys , f ))
729- scriptKernFeatureIndexes [script ] = thisScriptKernFeatureIndexes
731+ thisScriptDesiredFeatureIndexes .append ((langSys , f ))
732+ scriptDesiredFeatureIndexes [script ] = thisScriptDesiredFeatureIndexes
730733 # convert the feature indexes to lookup indexes
731734 scriptLookupIndexes = {}
732- for script , featureDefinitions in scriptKernFeatureIndexes .items ():
735+ for script , featureDefinitions in scriptDesiredFeatureIndexes .items ():
733736 lookupIndexes = scriptLookupIndexes [script ] = []
734737 for language , featureIndexes in featureDefinitions :
735738 for featureIndex in featureIndexes :
@@ -1085,3 +1088,104 @@ def extractOpenTypeFeatures(source):
10851088 if _haveFontFeatures :
10861089 return unparse (source ).asFea ()
10871090 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