Created
February 19, 2025 18:29
-
-
Save connordavenport/89f48283e95318989fe5fae3235193df to your computer and use it in GitHub Desktop.
add smart sets from internal feature file on opening
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from mojo.subscriber import Subscriber, registerRoboFontSubscriber | |
from vanilla import * | |
import AppKit | |
import os | |
from compositor.textUtilities import convertCase | |
from defconAppKit.windows.baseWindow import BaseWindowController | |
from defconAppKit.controls.openTypeControlsView import OpenTypeControlsView | |
from defconAppKit.controls.glyphSequenceEditText import GlyphSequenceEditText | |
from defconAppKit.controls.glyphLineView import GlyphLineView | |
from ufo2fdk.makeotfParts import forceAbsoluteIncludesInFeatures, extractFeaturesAndTables | |
from ufo2ft.featureWriters.kernFeatureWriter import KernFeatureWriter, ast | |
from ufo2ft.util import makeOfficialGlyphOrder | |
import uharfbuzz as hb | |
from fontTools import unicodedata | |
from fontTools.feaLib.parser import Parser as FeatureParser | |
from fontTools.fontBuilder import FontBuilder | |
import io | |
# `FeatureFont` code from FeaturePreview.roboFontExt, thanks Tal and Frederik! | |
class Table(object): | |
def wrapValue(self, attribute, value): | |
def callback(): | |
return value | |
setattr(self, attribute, callback) | |
class FeatureFont(object): | |
def __init__(self, font): | |
self.font = font | |
self.buildCMAP() | |
self.buildBinaryFont() | |
self.loadFeatures() | |
self.loadStylisticSetNames() | |
self.loadAlternates() | |
def buildCMAP(self): | |
font = self.font | |
self.cmap = {uni: names[0] for uni, names in font.unicodeData.items()} | |
# add all glyphs, even the un encoded ones at a high unicode... | |
# see https://github.com/harfbuzz/uharfbuzz/issues/22 | |
unicodeOffset = 0x110000 | |
unencodedCount = 0 | |
for glyph in font: | |
if not glyph.unicodes: | |
self.cmap[unicodeOffset + unencodedCount] = glyph.name | |
unencodedCount += 1 | |
def buildBinaryFont(self): | |
font = self.font | |
glyphOrder = sorted(set(font.glyphOrder) | set(self.cmap.values())) | |
ff = FontBuilder(int(round(font.info.unitsPerEm)), isTTF=True) | |
ff.setupGlyphOrder(glyphOrder) | |
ff.addOpenTypeFeatures(self._getFeatureText(font)) | |
data = io.BytesIO() | |
ff.save(data) | |
self.source = ff.font | |
self._data = data.getvalue() | |
def loadFeatures(self): | |
ft = self.source | |
self.gsub = None | |
if "GSUB" in ft and ft["GSUB"].table.FeatureList is not None: | |
self.gsub = Table() | |
GSUBFeatureTags = set() | |
GSUBScriptList = set() | |
GSUBLanguageList = set() | |
for record in ft["GSUB"].table.FeatureList.FeatureRecord: | |
GSUBFeatureTags.add(record.FeatureTag) | |
for record in ft["GSUB"].table.ScriptList.ScriptRecord: | |
GSUBScriptList.add(record.ScriptTag) | |
script = record.Script | |
if script.LangSysCount: | |
for langSysRecord in script.LangSysRecord: | |
GSUBLanguageList.add(langSysRecord.LangSysTag) | |
self.gsub.wrapValue("getFeatureList", list(sorted(GSUBFeatureTags))) | |
def loadStylisticSetNames(self): | |
ft = self.source | |
self.stylisticSetNames = dict() | |
if "GSUB" in ft and ft["GSUB"].table.FeatureList is not None: | |
# names | |
nameIDs = {} | |
if "name" in ft: | |
for nameRecord in ft["name"].names: | |
nameID = nameRecord.nameID | |
platformID = nameRecord.platformID | |
platEncID = nameRecord.platEncID | |
langID = nameRecord.langID | |
nameIDs[nameID, platformID, platEncID, langID] = nameRecord.toUnicode() | |
for record in ft["GSUB"].table.FeatureList.FeatureRecord: | |
params = record.Feature.FeatureParams | |
if hasattr(params, "UINameID"): | |
ssNameID = params.UINameID | |
namePriority = [(ssNameID, 1, 0, 0), (ssNameID, 1, None, None), (ssNameID, 3, 1, 1033), (ssNameID, 3, None, None)] | |
ssName = self._skimNameIDs(nameIDs, namePriority) | |
if ssName: | |
self.stylisticSetNames[record.FeatureTag] = ssName | |
def loadAlternates(self): | |
self.alternates = {} | |
ft = self.source | |
if "GSUB" in ft: | |
lookup = ft["GSUB"].table.LookupList.Lookup | |
for record in ft["GSUB"].table.FeatureList.FeatureRecord: | |
if record.FeatureTag == "aalt": | |
for lookupIndex in record.Feature.LookupListIndex: | |
for subTable in lookup[lookupIndex].SubTable: | |
if subTable.LookupType == 1: | |
for key, value in subTable.mapping.items(): | |
if key not in self.alternates: | |
self.alternates[key] = set() | |
self.alternates[key].add(value) | |
elif subTable.LookupType == 3: | |
for key, values in subTable.alternates.items(): | |
if key not in self.alternates: | |
self.alternates[key] = set() | |
self.alternates[key] |= set(values) | |
def _getFeatureText(self, font): | |
if font.path is None: | |
fea = font.features.text | |
featuretags, _ = extractFeaturesAndTables(fea) | |
else: | |
fea = forceAbsoluteIncludesInFeatures(font.features.text, os.path.dirname(font.path)) | |
featuretags, _ = extractFeaturesAndTables(fea, scannedFiles=[os.path.join(font.path, "features.fea")]) | |
if "kern" not in featuretags: | |
languageSystems = set() | |
for glyph in font: | |
for uni in glyph.unicodes: | |
scriptTag = unicodedata.script(chr(uni)) | |
languageSystems.add(scriptTag.lower()) | |
languageSystems -= set(["common", "zyyy", "zinh", "zzzz"]) | |
languageSystems = ["DFLT"] + sorted(languageSystems) | |
data = io.StringIO(fea) | |
feaParser = FeatureParser(data, set(font.keys())) | |
feaFile = feaParser.parse() | |
existingLanguageSystems = set() | |
DFLTindex = 0 | |
# search for existing language systems | |
# and keep the index of the DFLT if existing | |
for index, st in enumerate(feaFile.statements): | |
if isinstance(st, ast.LanguageSystemStatement): | |
existingLanguageSystems.add(st.script) | |
if st.script == "DFLT": | |
DFLTindex = index + 1 | |
addedStatement = [] | |
for script in reversed(languageSystems): | |
if script not in existingLanguageSystems: | |
statement = ast.LanguageSystemStatement(script=script, language="dflt") | |
addedStatement.append(statement) | |
feaFile.statements.insert(DFLTindex, statement) | |
writer = KernFeatureWriter() | |
def _kernFeatureWriterSetOrderedGlyphSet(): | |
"""Return OrderedDict[glyphName, glyph] sorted by glyphOrder.""" | |
glyphOrder = makeOfficialGlyphOrder(font, font.glyphOrder) | |
return {glyphName: font[glyphName] for glyphName in glyphOrder} | |
writer.getOrderedGlyphSet = _kernFeatureWriterSetOrderedGlyphSet | |
writer.write(font, feaFile) | |
# clean up | |
for statement in addedStatement: | |
feaFile.statements.remove(statement) | |
def removeScriptlanguage(block): | |
for statement in list(block.statements): | |
if hasattr(statement, "statements"): | |
removeScriptlanguage(statement) | |
if isinstance(statement, (ast.ScriptStatement, ast.LanguageStatement)): | |
block.statements.remove(statement) | |
for block in ast.iterFeatureBlocks(feaFile, tag="kern"): | |
removeScriptlanguage(block) | |
fea = feaFile.asFea() | |
return fea | |
def _skimNameIDs(self, nameIDs, priority): | |
for (nameID, platformID, platEncID, langID) in priority: | |
for (nID, pID, pEID, lID), text in nameIDs.items(): | |
if nID != nameID: | |
continue | |
if pID != platformID and platformID is not None: | |
continue | |
if pEID != platEncID and platEncID is not None: | |
continue | |
if lID != langID and langID is not None: | |
continue | |
return text | |
class set_fea_sets(Subscriber): | |
def fontDocumentWillOpen(self,info): | |
font = info["font"] | |
ff = FeatureFont(font.naked()) | |
#query = [] | |
query = font.lib.get("com.typemytype.robofont.smartSets",[]) | |
current = [Q["smartSetName"] for Q in query] | |
for tag in ff.gsub.getFeatureList(): | |
subq = {} | |
GSUB = ff.source["GSUB"] | |
for r in GSUB.table.FeatureList.FeatureRecord: | |
if r.Feature.LookupListIndex: | |
if r.FeatureTag == tag: | |
Lookup = GSUB.table.LookupList.Lookup[r.Feature.LookupListIndex[0]] | |
for Subtable in Lookup.SubTable: | |
if Subtable.LookupType == 1: | |
mapping = list(Subtable.mapping.values()) | |
name = f"{tag} {ff.stylisticSetNames.get(tag, "")}".strip() | |
# overwrite old sets | |
if name in current: | |
ii = [i for i in query if i.get("smartSetName", "") == name] | |
if ii: | |
query.remove(ii[0]) | |
subq["smartSetName"] = name | |
subq["glyphNames"] = mapping | |
query.append(subq) | |
if query: | |
font.lib["com.typemytype.robofont.smartSets"] = query | |
registerRoboFontSubscriber(set_fea_sets) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment