Last active
June 12, 2025 13:00
-
-
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
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
| """ | |
| 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