"""Add Word same-run prime forms shifted by preceding MATH italic correction. Use padding and measurements from the local build and generated font. """ from __future__ import annotations import argparse from fractions import Fraction import unicodedata from fontTools.ttLib import TTFont from fontTools.ttLib.tables import otTables as ot from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.t2CharStringPen import T2CharStringPen from fontTools.pens.transformPen import TransformPen # Context classes are a compact Word-compatibility approximation, expressed as # fractions of the output em rather than copied MTPro2 measurements. _CLASS_STEP = Fraction(3, 100) _CLASS_CAP = Fraction(27, 100) def _scaled_policy(upm: int) -> tuple[int, int]: step = max(1, round(_CLASS_STEP * upm)) cap = max(step, round(_CLASS_CAP * upm)) return step, cap def apply(path: str, *, base_shift: int) -> None: f = TTFont(path) if 'MATH' not in f or 'GSUB' not in f: return if 'CFF ' not in f: raise ValueError('prime contextualization expects the generated CFF OTF') cmap = f.getBestCmap() gi = f['MATH'].table.MathGlyphInfo icinfo = gi.MathItalicsCorrectionInfo ics = dict(zip(icinfo.Coverage.glyphs, [v.Value for v in icinfo.ItalicsCorrection])) step, cap = _scaled_policy(f['head'].unitsPerEm) # Only encoded mathematical letters participate. Operators and punctuation # follow their own OpenType MATH placement paths. classes: dict[int, list[str]] = {} for u, g in cmap.items(): if u < 0x370: continue try: category = unicodedata.category(chr(u)) except ValueError: continue if not category.startswith('L'): continue ic = int(ics.get(g, 0) or 0) klass = max(0, min(cap, int(round(ic / step)) * step)) if abs(klass - base_shift) < step / 2: continue classes.setdefault(klass, []).append(g) if not classes: return primes = [g for g in (cmap.get(0x2032), cmap.get(0x2033), cmap.get(0x2034), cmap.get(0x2057)) if g] if not primes: return # Create translated copies. Width and left sidebearing move by the same # delta, so the outline-to-advance relationship is preserved exactly. cff = f['CFF '].cff[f['CFF '].cff.fontNames[0]] cs = cff.CharStrings hmtx = f['hmtx'] order = f.getGlyphOrder() glyphset = f.getGlyphSet() made: dict[tuple[str, int], str] = {} for klass in sorted(classes): dx = klass - int(base_shift) for src in primes: new = f'{src}.ic{klass}' rp = RecordingPen() glyphset[src].draw(rp) width = hmtx[src][0] + dx pen = T2CharStringPen(width, None) rp.replay(TransformPen(pen, (1, 0, 0, 1, dx, 0))) chstr = pen.getCharString(private=cs[src].private, globalSubrs=cs.globalSubrs) cs.charStringsIndex.append(chstr) cs.charStrings[new] = len(cs.charStringsIndex) - 1 hmtx[new] = (width, hmtx[src][1] + dx) order.append(new) made[(src, klass)] = new f.setGlyphOrder(order) cff.charset = order f['maxp'].numGlyphs = len(order) gsub = f['GSUB'].table lookups = gsub.LookupList.Lookup def single_lookup(mapping): st = ot.SingleSubst() st.mapping = dict(mapping) lk = ot.Lookup() lk.LookupType = 1 lk.LookupFlag = 0 lk.SubTable = [st] lk.SubTableCount = 1 lookups.append(lk) return len(lookups) - 1 def coverage(glyphs): c = ot.Coverage() c.glyphs = sorted(set(glyphs), key=f.getGlyphID) return c ctx_indices = [] for klass in sorted(classes): sub_idx = single_lookup({src: made[(src, klass)] for src in primes}) chain = ot.ChainContextSubst() chain.Format = 3 chain.BacktrackGlyphCount = 1 chain.BacktrackCoverage = [coverage(classes[klass])] chain.InputGlyphCount = 1 chain.InputCoverage = [coverage(primes)] chain.LookAheadGlyphCount = 0 chain.LookAheadCoverage = [] rec = ot.SubstLookupRecord() rec.SequenceIndex = 0 rec.LookupListIndex = sub_idx chain.SubstLookupRecord = [rec] chain.SubstCount = 1 lk = ot.Lookup() lk.LookupType = 6 lk.LookupFlag = 0 lk.SubTable = [chain] lk.SubTableCount = 1 lookups.append(lk) ctx_indices.append(len(lookups) - 1) gsub.LookupList.LookupCount = len(lookups) for fr in gsub.FeatureList.FeatureRecord: if fr.FeatureTag == 'ccmp': fr.Feature.LookupListIndex.extend(ctx_indices) fr.Feature.LookupCount = len(fr.Feature.LookupListIndex) f.save(path) def main() -> None: ap = argparse.ArgumentParser() ap.add_argument('font') ap.add_argument('--base-shift', required=True, type=int, help='local source-derived raw-prime padding') ns = ap.parse_args() apply(ns.font, base_shift=ns.base_shift) if __name__ == '__main__': main()