Skip to content

Commit 124a25b

Browse files
committed
Implement anchor extraction from OpenType files
Fixes #67
1 parent f2ff494 commit 124a25b

File tree

1 file changed

+128
-17
lines changed

1 file changed

+128
-17
lines changed

Diff for: Lib/extractor/formats/opentype.py

+128-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
from defcon import Anchor
23
from fontTools.pens.boundsPen import ControlBoundsPen
34
from fontTools.pens.hashPointPen import HashPointPen
45
from fontTools.ttLib import TTFont, TTLibError
@@ -43,6 +44,7 @@ def extractFontFromOpenType(
4344
doFeatures=True,
4445
customFunctions=[],
4546
doInstructions=True,
47+
doAnchors=True,
4648
):
4749
source = TTFont(pathOrFile)
4850
if doInfo:
@@ -64,6 +66,8 @@ def extractFontFromOpenType(
6466
function(source, destination)
6567
if doInstructions:
6668
extractInstructions(source, destination)
69+
if doAnchors:
70+
extractAnchors(source, destination)
6771
source.close()
6872

6973

@@ -595,7 +599,7 @@ def _extractOpenTypeKerningFromGPOS(source):
595599
kerningDictionaries,
596600
leftClassDictionaries,
597601
rightClassDictionaries,
598-
) = _gatherDataFromLookups(gpos, scriptOrder)
602+
) = _gatherKerningDataFromLookups(gpos, scriptOrder)
599603
# merge all kerning pairs
600604
kerning = _mergeKerningDictionaries(kerningDictionaries)
601605
# get rid of groups that have only one member
@@ -647,12 +651,12 @@ def _makeScriptOrder(gpos):
647651
return sorted(scripts)
648652

649653

650-
def _gatherDataFromLookups(gpos, scriptOrder):
654+
def _gatherKerningDataFromLookups(gpos, scriptOrder):
651655
"""
652656
Gather kerning and classes from the applicable lookups
653657
and return them in script order.
654658
"""
655-
lookupIndexes = _gatherLookupIndexes(gpos)
659+
lookupIndexes = _gatherLookupIndexes(gpos, ["kern"])
656660
seenLookups = set()
657661
kerningDictionaries = []
658662
leftClassDictionaries = []
@@ -679,50 +683,50 @@ def _gatherDataFromLookups(gpos, scriptOrder):
679683
return kerningDictionaries, leftClassDictionaries, rightClassDictionaries
680684

681685

682-
def _gatherLookupIndexes(gpos):
686+
def _gatherLookupIndexes(gpos, featureTags):
683687
"""
684688
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.
686690
Returns a dictionary of this structure:
687691
{
688692
"latn" : [0],
689693
"DFLT" : [0]
690694
}
691695
"""
692-
# gather the indexes of the kern features
693-
kernFeatureIndexes = [
696+
# gather the indexes of the desired features
697+
desiredFeatureIndexes = [
694698
index
695699
for index, featureRecord in enumerate(gpos.FeatureList.FeatureRecord)
696-
if featureRecord.FeatureTag == "kern"
700+
if featureRecord.FeatureTag in featureTags
697701
]
698-
# find scripts and languages that have kern features
699-
scriptKernFeatureIndexes = {}
702+
# find scripts and languages that have desired features
703+
scriptDesiredFeatureIndexes = {}
700704
for scriptRecord in gpos.ScriptList.ScriptRecord:
701705
script = scriptRecord.ScriptTag
702-
thisScriptKernFeatureIndexes = []
706+
thisScriptDesiredFeatureIndexes = []
703707
defaultLangSysRecord = scriptRecord.Script.DefaultLangSys
704708
if defaultLangSysRecord is not None:
705709
f = []
706710
for featureIndex in defaultLangSysRecord.FeatureIndex:
707-
if featureIndex not in kernFeatureIndexes:
711+
if featureIndex not in desiredFeatureIndexes:
708712
continue
709713
f.append(featureIndex)
710714
if f:
711-
thisScriptKernFeatureIndexes.append((None, f))
715+
thisScriptDesiredFeatureIndexes.append((None, f))
712716
if scriptRecord.Script.LangSysRecord is not None:
713717
for langSysRecord in scriptRecord.Script.LangSysRecord:
714718
langSys = langSysRecord.LangSysTag
715719
f = []
716720
for featureIndex in langSysRecord.LangSys.FeatureIndex:
717-
if featureIndex not in kernFeatureIndexes:
721+
if featureIndex not in desiredFeatureIndexes:
718722
continue
719723
f.append(featureIndex)
720724
if f:
721-
thisScriptKernFeatureIndexes.append((langSys, f))
722-
scriptKernFeatureIndexes[script] = thisScriptKernFeatureIndexes
725+
thisScriptDesiredFeatureIndexes.append((langSys, f))
726+
scriptDesiredFeatureIndexes[script] = thisScriptDesiredFeatureIndexes
723727
# convert the feature indexes to lookup indexes
724728
scriptLookupIndexes = {}
725-
for script, featureDefinitions in scriptKernFeatureIndexes.items():
729+
for script, featureDefinitions in scriptDesiredFeatureIndexes.items():
726730
lookupIndexes = scriptLookupIndexes[script] = []
727731
for language, featureIndexes in featureDefinitions:
728732
for featureIndex in featureIndexes:
@@ -1078,3 +1082,110 @@ def extractOpenTypeFeatures(source):
10781082
if _haveFontFeatures:
10791083
return unparse(source).asFea()
10801084
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

Comments
 (0)