Last active
January 29, 2025 00:30
-
-
Save njourdane/fdb13b4344ef4b5bdb3278b24f88bf2a to your computer and use it in GitHub Desktop.
Ligature builder proof of concept
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
# based on https://github.com/ToxicFrog/Ligaturizer/blob/master/ligaturize.py | |
import fontforge | |
import psMat | |
from pathlib import Path | |
INPUT_FONT_PATH = Path("/usr/share/fonts/truetype/comfortaa/Comfortaa-Regular.ttf") | |
FONT_NAME = 'A ligature test font' | |
OUTPUT_DIR = Path(__file__).parent / "output" | |
LIGATURES = [ | |
{ | |
'chars': ['o', 'e'], | |
'ligature_name': 'oe', | |
}, | |
] | |
FEATURE_SCRIPT_LANG_TUPLE = ( | |
('calt', ( | |
('DFLT', ('dflt',)), | |
('arab', ('dflt',)), | |
('armn', ('dflt',)), | |
('cyrl', ('SRB ', 'dflt')), | |
('geor', ('dflt',)), | |
('grek', ('dflt',)), | |
('lao ', ('dflt',)), | |
('latn', ('CAT ', 'ESP ', 'GAL ', 'ISM ', 'KSM ', 'LSM ', 'MOL ', 'NSM ', 'ROM ', 'SKS ', 'SSM ', 'dflt')), | |
('math', ('dflt',)), | |
('thai', ('dflt',)) | |
)), | |
) | |
class LigatureCreator(object): | |
def __init__(self, font_path, font_name, output_dir): | |
self.font_path = font_path | |
self.font_name = font_name | |
self.output_dir = output_dir | |
self.font = fontforge.open(str(self.font_path)) | |
self._lig_counter = 0 | |
def add_ligature(self, input_chars, ligature_name): | |
self._lig_counter += 1 | |
lookup_name_tmpl = f'lookup.{ self._lig_counter }.%d' | |
lookup_sub_name_tmpl = f'lookup.sub.{ self._lig_counter }.%d' | |
cr_name_tmpl = f'CR.{ self._lig_counter }.%d' | |
self.font.selection.none() | |
self.font.selection.select(ligature_name) | |
self.font.copy() | |
self.font.createChar(-1, ligature_name) | |
self.font.selection.none() | |
self.font.selection.select(ligature_name) | |
self.font.paste() | |
self.font.selection.none() | |
self.font.selection.select('space') | |
self.font.copy() | |
for i, char in enumerate(input_chars): # ["period", "e"] | |
self.font.addLookup(lookup_name_tmpl % i, 'gsub_single', (), ()) | |
self.font.addLookupSubtable(lookup_name_tmpl % i, lookup_sub_name_tmpl % i) | |
if i < len(input_chars) - 1: | |
self.font.createChar(-1, cr_name_tmpl % i) | |
self.font.selection.none() | |
self.font.selection.select(cr_name_tmpl % i) | |
self.font.paste() | |
self.font[char].addPosSub(lookup_sub_name_tmpl % i, cr_name_tmpl % i) | |
else: | |
self.font[char].addPosSub(lookup_sub_name_tmpl % i, ligature_name) | |
calt_lookup_name = f'calt.{ self._lig_counter }' | |
self.font.addLookup(calt_lookup_name, 'gsub_contextchain', (), FEATURE_SCRIPT_LANG_TUPLE) | |
print('CALT %s (%s)' % (calt_lookup_name, ligature_name)) | |
for i, char in enumerate(input_chars): | |
prev = ' '.join(cr_name_tmpl % j for j in range(i)) | |
next = ' '.join(input_chars[i+1:]) | |
lookup = lookup_name_tmpl % i | |
subtable_name = f'calt.{ self._lig_counter }.{ i }' | |
rule = f'{ prev } | { char } @<{ lookup }> | { next }' | |
print(subtable_name, ":", rule) | |
self.font.addContextualSubtable(calt_lookup_name, subtable_name, 'glyph', rule) | |
def replace_sfnt(self, key, value): | |
self.font.sfnt_names = tuple( | |
(row[0], key, value) | |
if row[1] == key | |
else row | |
for row in self.font.sfnt_names | |
) | |
def update_font_metadata(self): | |
old_name = self.font.familyname | |
clean_name = self.font_name.replace(' ', '') | |
suffix = (self.font.fontname.split('-', 1) + [''])[1] | |
self.font.familyname = self.font_name | |
self.font.fullname = "%s %s" % (self.font_name, suffix) if suffix else self.font_name | |
self.font.fontname = "%s-%s" % (clean_name, suffix) if suffix else clean_name | |
path_name = Path(self.font.path).name | |
print(f"Ligaturizing font { path_name } ({ old_name }) as '{ self.font_name }'") | |
# self.font.copyright = (self.font.copyright or '') + COPYRIGHT | |
self.replace_sfnt('UniqueID', '%s; Ligaturized' % self.font.fullname) | |
self.replace_sfnt('Preferred Family', self.font_name) | |
self.replace_sfnt('Compatible Full', self.font_name) | |
self.replace_sfnt('Family', self.font_name) | |
self.replace_sfnt('WWS Family', self.font_name) | |
def ligaturize_font(self): | |
self.update_font_metadata() | |
ligature_length = lambda lig: len(lig['chars']) | |
for lig_spec in sorted(LIGATURES, key = ligature_length): | |
try: | |
self.add_ligature(lig_spec['chars'], lig_spec['ligature_name']) | |
except Exception as e: | |
print('Exception while adding ligature: {}'.format(lig_spec)) | |
raise | |
# Work around a bug in Fontforge where the underline height is subtracted from | |
# the underline width when you call generate(). | |
self.font.upos += self.font.uwidth | |
output_font_type = '.otf' if self.font_path.suffix.lower() == '.otf' else '.ttf' | |
output_font_file = self.output_dir / (self.font.fontname + output_font_type) | |
print(f"saving to '{ output_font_file }' ({ self.font.fullname })") | |
self.font.generate(str(output_font_file)) | |
if __name__ == '__main__': | |
ligature_creator = LigatureCreator(INPUT_FONT_PATH, FONT_NAME, OUTPUT_DIR) | |
ligature_creator.ligaturize_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
""" | |
Utility script that: | |
1. Build a svg file containing a text; | |
2. Copy font file to user font dir; | |
3. Convert the svg to png with Inkscape. | |
Note: cairosvg does not support ligatures but Inkscape does, hence the system call. | |
""" | |
from pathlib import Path | |
import subprocess | |
FONT_DIR = Path.home() / '.fonts' | |
OUTPUT_DIR = Path('./output') | |
SVG_PATH = OUTPUT_DIR / 'preview.svg' | |
PNG_PATH = OUTPUT_DIR / 'preview.png' | |
FONT_PATH = OUTPUT_DIR / 'Aligaturetestfont-Regular.ttf' | |
FONT_NAME = "A ligature test font" | |
TEXT = "noeud" | |
WIDTH, HEIGHT = 300, 200 | |
CSS = f''' | |
text {{ | |
font-family: "{ FONT_NAME }"; | |
text-anchor: middle; | |
alignment-baseline: middle; | |
fill: darkslategrey; | |
font-size: 60; | |
}} | |
''' | |
SVG_PREFIX = f'''<?xml version="1.0" encoding="utf-8"?> | |
<svg xmlns="http://www.w3.org/2000/svg" width="{ WIDTH }" height="{ HEIGHT }"> | |
<style>\n{ CSS }</style>\n | |
''' | |
SVG_SUFFIX = ''' | |
</svg> | |
''' | |
CMD = f"inkscape { SVG_PATH } --export-filename={ PNG_PATH }" | |
svg = f'<text x="{ WIDTH/2 }px" y="{ HEIGHT/2 }px">{ TEXT }</text>' | |
SVG_PATH.parent.mkdir(parents=True, exist_ok=True) | |
with open(SVG_PATH, 'w', encoding='utf-8') as svg_file: | |
svg_file.write(f'{ SVG_PREFIX }{ svg }{ SVG_SUFFIX }') | |
(FONT_DIR / FONT_PATH.name).write_bytes(FONT_PATH.read_bytes()) | |
subprocess.call(CMD, shell=True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment