Skip to content

Instantly share code, notes, and snippets.

@anthrotype
Last active June 12, 2025 13:00
Show Gist options
  • Select an option

  • Save anthrotype/f675616a17048c87cf43a8c2e4cbfd71 to your computer and use it in GitHub Desktop.

Select an option

Save anthrotype/f675616a17048c87cf43a8c2e4cbfd71 to your computer and use it in GitHub Desktop.
Example script for Cibu using fontTools to add blwm feature to an existing font
"""
Usage:
python add_ot_feature.py Sulekha.ttf Sulekha_new.ttf
"""
import sys
from fontTools.ttLib import TTFont
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
# This is hard-coded but it could be read from a file
FEATURES = """\
# adjust the anchor's (x, y) positions as needed, I made those numbers up
# `markClass` statements define a group of mark glyphs that share the same anchor.
# You can either add more mark glyphs to the same class of marks if they
# share the same anchor position, or define new mark classes below
markClass [u1] <anchor 30 50> @BOTTOM_MARKS;
# this block defines a new MarkToBase lookup
lookup blwm_mark2base {
# you can add as many additional base glyphs as you want below
pos base k1 <anchor 900 0> mark @BOTTOM_MARKS;
} blwm_mark2base;
# this defines a MarkToLigature lookup. The glyph 'k1k1' is actually a ligature according
# to the 'Sulekha.ttf' font's GSUB table, and classified as such in the GDEF.GlyphClassDef,
# thus it's more appropriate to add it to mark2ligature than mark2base lookup.
lookup blmw_mark2liga {
pos ligature k1k1
# the NULL means the first ligature component doesn't have any marks attached.
# replace it with actual anchor positions if you want.
<anchor NULL>
# a `ligComponent` statement starts the anchor list for the subsequent ligature component, etc.
ligComponent
<anchor 1250 0> mark @BOTTOM_MARKS;
} blmw_mark2liga;
# this defines a new 'blwm' feature which references the lookups above and registers itself
# under a set of scripts/languages
feature blwm {
# below I register the 'blwm_mark2base' lookup defined above under the 'blwm' feature,
# and in turn register this feature for all the scripts and languages listed below.
# I use explicit script/language declarations in the feature block instead of relying
# on the global, implicit `languagesystems` statements.
script DFLT;
# technically `language dflt` is optional as it's the default when a new `script XXXX`
# is declared but I'll add it to be more explicit.
language dflt;
lookup blwm_mark2base;
lookup blmw_mark2liga;
# what's this 'MAL ' script that is in the 'Sulekha.ttf' font?!
# It's not among OpenType official script tags:
# https://learn.microsoft.com/en-us/typography/opentype/spec/scripttags
# I'll include it here anyway for completeness.
script MAL;
language dflt;
lookup blwm_mark2base;
lookup blmw_mark2liga;
script mlm2;
language dflt;
lookup blwm_mark2base;
lookup blmw_mark2liga;
script mlym;
language dflt;
lookup blwm_mark2base;
lookup blmw_mark2liga;
} blwm;
"""
if len(sys.argv) < 3:
print("usage: add_ot_features.py input.ttf output.ttf")
sys.exit(1)
input_font = TTFont(sys.argv[1])
# create a new *empty* font with the same glyph order; this is sufficient to
# compile the OT layout tables
temp_font = TTFont()
temp_font.setGlyphOrder(input_font.getGlyphOrder())
# this compiles the OT layout tables (GPOS, GDEF, etc.) from the features
addOpenTypeFeaturesFromString(temp_font, FEATURES)
gpos1 = input_font["GPOS"].table
gpos2 = temp_font["GPOS"].table
# copy the new feature records at the end of the existing feature list and
# increment the lookup indices
feature_index_start = gpos1.FeatureList.FeatureCount
lookup_index_start = gpos1.LookupList.LookupCount
for feature_rec in gpos2.FeatureList.FeatureRecord:
feature_rec.Feature.LookupListIndex = [
lookup_index_start + i for i in feature_rec.Feature.LookupListIndex
]
gpos1.FeatureList.FeatureRecord.append(feature_rec)
# update the feature count
gpos1.FeatureList.FeatureCount = len(gpos1.FeatureList.FeatureRecord)
# now copy the new lookups at the end of the existing lookup list
for lookup in gpos2.LookupList.Lookup:
gpos1.LookupList.Lookup.append(lookup)
# update the lookup count
gpos1.LookupList.LookupCount = len(gpos1.LookupList.Lookup)
# we assume the original font and the temporary one have the same number of script records
# so we can zip them for incrementing the feature indices.
# We assume that we want to register the new features under all scripts/languages
assert len(gpos1.ScriptList.ScriptRecord) == len(gpos2.ScriptList.ScriptRecord)
for script_rec_1, script_rec_2 in zip(
gpos1.ScriptList.ScriptRecord, gpos2.ScriptList.ScriptRecord
):
script_rec_1.Script.DefaultLangSys.FeatureIndex += [
feature_index_start + i for i in script_rec_2.Script.DefaultLangSys.FeatureIndex
]
# Finally update the GDEF table's GlyphClassDef with any new bases/marks/ligatures
gdef1 = input_font["GDEF"].table
gdef2 = temp_font["GDEF"].table
classDefs2 = gdef2.GlyphClassDef.classDefs
gdef1.GlyphClassDef.classDefs.update(classDefs2)
# Save the updated font
input_font.save(sys.argv[2])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment