import argparse
import sys
import unicodedata
from collections import Counter
from fontTools.pens.transformPen import TransformPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables._c_m_a_p import CmapSubtable
MATH_RANGES = [
(0x00A8, 0x00AF), (0x02C6, 0x02DF), (0x0370, 0x03FF), (0x1D00, 0x1D7F), (0x2C60, 0x2C7F), (0x2010, 0x205F), (0x2070, 0x209F), (0x20D0, 0x20FF), (0x2100, 0x214F), (0x2190, 0x21FF), (0x2200, 0x22FF), (0x2300, 0x23FF), (0x2460, 0x24FF), (0x2500, 0x257F), (0x2580, 0x259F), (0x25A0, 0x25FF), (0x2700, 0x27BF), (0x27C0, 0x27EF), (0x27F0, 0x27FF), (0x2900, 0x297F), (0x2980, 0x29FF), (0x2A00, 0x2AFF), (0x2B00, 0x2BFF), (0x1D400, 0x1D7FF), ]
def cells_wide(cp: int) -> int:
return 2 if unicodedata.east_asian_width(chr(cp)) in ("W", "F") else 1
def mono_advance(font: TTFont) -> int:
cmap = font.getBestCmap()
hmtx = font["hmtx"]
widths = Counter(
hmtx[cmap[cp]][0] for cp in range(0x21, 0x7F) if cp in cmap
)
if not widths:
sys.exit("base font has no ASCII glyphs; is this a text font?")
return widths.most_common(1)[0][0]
def copy_glyph(src: TTFont, dst: TTFont, src_name: str, dst_name: str,
scale: float, target_advance: int) -> None:
src_glyphset = src.getGlyphSet()
pen = TTGlyphPen(None) src_glyphset[src_name].draw(TransformPen(pen, (scale, 0, 0, scale, 0, 0)))
glyph = pen.glyph()
dst["glyf"][dst_name] = glyph
lsb = 0
if glyph.numberOfContours:
glyph.recalcBounds(dst["glyf"])
lsb = glyph.xMin
dst["hmtx"][dst_name] = (target_advance, lsb)
def rebuild_cmap(font: TTFont, mapping: dict) -> None:
cmap = font["cmap"]
bmp = {cp: n for cp, n in mapping.items() if cp <= 0xFFFF}
sub4 = CmapSubtable.getSubtableClass(4)(4)
sub4.platformID, sub4.platEncID, sub4.language = 3, 1, 0
sub4.cmap = bmp
sub12 = CmapSubtable.getSubtableClass(12)(12)
sub12.platformID, sub12.platEncID, sub12.language = 3, 10, 0
sub12.format, sub12.reserved, sub12.length, sub12.nGroups = 12, 0, 0, 0
sub12.cmap = dict(mapping)
cmap.tableVersion = 0
cmap.tables = [sub4, sub12]
def rename_font(font: TTFont, family: str) -> None:
name = font["name"]
ps = family.replace(" ", "")
for nid, value in ((1, family), (3, f"{ps}:formulaa-merged"), (4, family),
(6, ps), (16, family)):
name.removeNames(nameID=nid)
name.setName(value, nid, 3, 1, 0x409) name.setName(value, nid, 1, 0, 0)
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("base", help="base monospace font (.ttf/.ttc, glyf outlines)")
ap.add_argument("-j", "--julia", action="append", default=None,
help="donor TTF; repeatable, first donor that has a "
"codepoint wins (default: ./JuliaMono-Regular.ttf)")
ap.add_argument("-o", "--output", default=None,
help="output path (default: <base stem>-Math.ttf)")
ap.add_argument("--font-number", type=int, default=0,
help="face index inside a .ttc collection")
ap.add_argument("--all-missing", action="store_true",
help="copy every codepoint the base font lacks")
ap.add_argument("--family", default=None,
help="output family name (default: '<Base> Math')")
args = ap.parse_args()
base = TTFont(args.base, fontNumber=args.font_number
if args.base.lower().endswith(".ttc") else -1)
donors = [TTFont(p) for p in (args.julia or ["JuliaMono-Regular.ttf"])]
if "glyf" not in base:
sys.exit("base font has no glyf table (CFF/OTF is not supported)")
base_cmap = base.getBestCmap()
donor_of = {}
for d in reversed(donors):
donor_of.update(dict.fromkeys(d.getBestCmap(), d))
base_adv = mono_advance(base)
if args.all_missing:
candidates = set(donor_of)
else:
candidates = {
cp
for lo, hi in MATH_RANGES
for cp in range(lo, hi + 1)
if cp in donor_of
}
todo = sorted(candidates - set(base_cmap))
if not todo:
sys.exit("nothing to merge: the base font already covers the ranges")
existing = set(base.getGlyphOrder())
mapping = dict(base_cmap)
added = 0
for cp in todo:
julia = donor_of[cp]
src_name = julia.getBestCmap()[cp]
src_adv = julia["hmtx"][src_name][0]
if src_adv == 0:
continue target = cells_wide(cp) * base_adv
scale = target / src_adv
dst_name = f"u{cp:04X}"
if dst_name in existing:
dst_name = f"u{cp:04X}.jm"
copy_glyph(julia, base, src_name, dst_name, scale, target)
existing.add(dst_name)
mapping[cp] = dst_name
added += 1
base.setGlyphOrder(base["glyf"].glyphOrder)
rebuild_cmap(base, mapping)
base["post"].formatType = 3.0
base["post"].glyphOrder = None
stem = args.base.rsplit("/", 1)[-1].rsplit(".", 1)[0]
family = args.family or f"{stem} Math"
rename_font(base, family)
out = args.output or f"{stem}-Math.ttf"
base.save(out)
names = ", ".join(p.rsplit("/", 1)[-1] for p in (args.julia or ["JuliaMono-Regular.ttf"]))
print(f"added {added} glyphs from {names} "
f"(cell width {base_adv}) -> {out}")
if __name__ == "__main__":
main()