Skip to content

Commit 52b706c

Browse files
committed
Implement anchor extraction from OpenType files
Fixes #67
1 parent 1289b9d commit 52b706c

File tree

1 file changed

+121
-17
lines changed

1 file changed

+121
-17
lines changed

Lib/extractor/formats/opentype.py

+121-17
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)