"""Build unified OpenType MATH from local MTPro2 sources and Roman donors. Run through build.sh with FontForge Python. """ import sys, os, json, math, subprocess from fractions import Fraction sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import fontforge, psMat from tfmpl import TFM import uni_map as U import math_constants import source_policy from build_config import identity ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) T1DIR = f'{ROOT}/mtpro2' TFMDIR = f'{ROOT}/mtpro2' OUTDIR = os.environ['MTP2_BUILD_DIR'] PLDIR = f'{OUTDIR}/pl' os.makedirs(PLDIR, exist_ok=True) os.makedirs(OUTDIR, exist_ok=True) # Editable embedding metadata. FSTYPE = 8 # Bound fixed variants for Windows; larger sizes use MATH assemblies. MAXVAR = 5580 MAXWIDTH = 12000 # Conversion tolerance for stable connector profiles. CONNECTOR_PROFILE_TOLERANCE = 8 EDITION = os.environ['MTP2_EDITION'] # Share source identities; read numeric policy from local inputs. SRC = source_policy.SOURCE_FONTS FAMILY, FONT_BASENAME, _ = identity(EDITION) FONT_PSNAME = FONT_BASENAME STYLE = 'Regular' def load(tag): name = SRC[tag] plp = f'{PLDIR}/{name}.pl' # Regenerate PL so the active TFM and PFB form one input set. subprocess.run( ['tftopl', f'{TFMDIR}/{name}.tfm', plp], check=True, stdout=subprocess.DEVNULL, ) tfm = TFM(plp) pfb = fontforge.open(f'{T1DIR}/{name}.pfb') slots = {g.encoding: g.glyphname for g in pfb.glyphs() if g.encoding < 0x10000} return pfb, tfm, slots fonts = {} _active_tags = source_policy.active_tags(EDITION) _load_errors = [] for tag in _active_tags: try: fonts[tag] = load(tag) except Exception as exc: _load_errors.append((tag, exc)) if _load_errors: details = '; '.join('%s(%s): %s' % (tag, SRC[tag], exc) for tag, exc in _load_errors) raise SystemExit('MTPro2 source load failed: ' + details) # Read the local snapshot prepared by pipeline.py before entering FontForge Python. _SOURCE_POLICY_PATH = os.environ.get('MTP2_SOURCE_POLICY_JSON') if not _SOURCE_POLICY_PATH or not os.path.isfile(_SOURCE_POLICY_PATH): raise SystemExit('missing local source snapshot; run this builder through ./build.sh') _SOURCE_SNAPSHOT = json.load(open(_SOURCE_POLICY_PATH, encoding='utf-8')) SOURCE_POLICY = source_policy.load_policy_json(_SOURCE_POLICY_PATH) SOURCE_SKEW = dict(SOURCE_POLICY.skewchar_by_tag) SCRIPT_RATIO = SOURCE_POLICY.script_ratio SCRIPTSCRIPT_RATIO = SOURCE_POLICY.scriptscript_ratio out = fontforge.font() out.encoding = 'UnicodeFull' out.em = 1000 out.familyname = FAMILY out.fontname = FONT_PSNAME out.fullname = FAMILY out.weight = 'Regular' out.copyright = ('Copyright (C) Publish or Perish, Inc. 2005. ' 'Outlines from the MathTime(TM) Professional II Type1 fonts. ' 'Unofficial OpenType MATH conversion for the licensee\'s own use; ' 'redistribution of this font file is not permitted.') imported = {} # (tag, slot) -> glyphname in out uni_owner = {} # unicode -> glyphname def uname(u): return 'uni%04X' % u if u < 0x10000 else 'u%05X' % u # Prefix digit-leading names for Word font subsetting. _DIGIT_NAME = ('zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine') def safe_gname(name): if name and name[0].isdigit(): return _DIGIT_NAME[int(name[0])] + name[1:] return name def _axis_height(): """Return the local MTPro2 math axis from the active symbol TFM.""" return round(fonts['syt'][1].fontdimen[22] * out.em) def _tfm_design_size(tfm): size = getattr(tfm, 'design_size', None) if not size or size <= 0: raise RuntimeError('local TFM is missing a usable DESIGNSIZE') return Fraction(str(size)) def _pt_to_units(value, tfm): """Convert a package dimension in TeX points to output-font units.""" return round(value / _tfm_design_size(tfm) * out.em) def _mu_to_units(value): """Convert TeX math units to output-font units (one em is eighteen mu).""" return round(value / 18 * out.em) def recenter_on_axis(gname): """Center delimiter variants on the math axis to bound Windows clipping metrics.""" g = out[gname] bb = g.boundingBox() if bb == (0, 0, 0, 0): return dy = _axis_height() - (bb[1] + bb[3]) / 2.0 if abs(dy) >= 1: g.transform(psMat.translate(0, round(dy))) def import_glyph(tag, slot, name=None, unicode_=None, force_alt=False): """Copy a source slot, or return its existing output glyph name.""" key = (tag, slot) if key in imported: gname = imported[key] if unicode_ is not None and unicode_ not in uni_owner and not force_alt: g = out[gname] if g.unicode == -1: g.unicode = unicode_ # Use codepoint-based names. nn = uname(unicode_) if nn not in out: g.glyphname = nn imported[key] = nn gname = nn else: out[gname].altuni = ((out[gname].altuni or ()) + ((unicode_, -1, 0),)) uni_owner[unicode_] = gname return gname pfb, tfm, slots = fonts[tag] if slot not in slots or slot not in tfm.chars: return None if unicode_ is not None and unicode_ in uni_owner: force_alt = True base = uni_owner[unicode_] unicode_ = None name = name or f'{base}.{tag}alt' if unicode_ is not None: gname = uname(unicode_) if name and not name.startswith('uni') and not name.startswith('u1'): gname = uname(unicode_) # Prefer the canonical codepoint name. else: gname = name or f'{tag}.{slot}' if gname in out: gname = f'{gname}.{tag}{slot}' gname = safe_gname(gname) pfb.selection.select(('encoding',), slot) pfb.copy() out.createChar(-1, gname) out.selection.select(gname) out.paste() g = out[gname] # Scale by the package-declared design size. scale = {'xxxl': 2.0, 'exe': 2.0, 'exf': 4.0, 'exg': 8.0}.get(tag, 1.0) if scale != 1.0: g.transform(psMat.scale(scale)) if tag in ('xl', 'xxxl'): # PFB vertical placement assumes TeX-side raising; recenter on the local math axis. bb = g.boundingBox() g.transform(psMat.translate(0, _axis_height() - (bb[1] + bb[3]) / 2)) _wd = tfm.chars[slot]['wd'] _ic = tfm.chars[slot]['ic'] if tag in ('exa', 'xl', 'xxxl') and _ic: # Include the slanted operator overhang in advance for OpenType script placement. g.width = round((_wd + _ic) * 1000 * scale) else: g.width = round(_wd * 1000 * scale) if unicode_ is not None: g.unicode = unicode_ uni_owner[unicode_] = gname ic = tfm.chars[slot]['ic'] if ic: g.italicCorrection = round(ic * 1000 * scale) imported[key] = gname return gname # Core Unicode mappings. plan = [('mit', U.MIT), ('syt', U.SYT)] for tag, table in plan: if tag not in fonts: continue for slot, (u, name) in sorted(table.items()): import_glyph(tag, slot, name=name, unicode_=u) # Encode text operators; keep display variants in size order. op_variants = {} # base glyph -> size-ordered variants, including the base if 'exa' in fonts: for slot, (u, name, dslot) in U.EXA_OPS.items(): base = import_glyph('exa', slot, name=name, unicode_=u) if base is None: continue var = [base] d = import_glyph('exa', dslot, name=f'{base}.dsp') if d: var.append(d) # xl / XL / XXL / XXXL if name in U.XL_TABLE and 'xl' in fonts: xl, XL, XXL, XXXL = U.XL_TABLE[name] for sz, (ftag, sl) in enumerate([('xl', xl), ('xl', XL), ('xl', XXL), ('xxxl', XXXL)]): if ftag not in fonts: continue sfx = ['.xl', '.XL', '.XXL', '.XXXL'][sz] if isinstance(sl, tuple): # Compose the two source halves. l = import_glyph(ftag, sl[0], name=f'{base}{sfx}.l') r = import_glyph(ftag, sl[1], name=f'{base}{sfx}.r') if l and r: gn = f'{base}{sfx}' out.createChar(-1, gn) g = out[gn] g.addReference(l) g.addReference(r, psMat.translate(out[l].width, 0)) g.width = out[l].width + out[r].width var.append(gn) else: v = import_glyph(ftag, sl, name=f'{base}{sfx}') if v: var.append(v) # Limit fixed sizes to keep Windows clipping bounds manageable. trimmed = [var[0]] for gn in var[1:]: bb = out[gn].boundingBox() if MAXVAR and bb[3] - bb[1] > MAXVAR: dead = {gn} | {r[0] for r in (out[gn].references or ())} for k, v in list(imported.items()): if v in dead: del imported[k] for d in dead: if d in out: out.removeGlyph(out[d]) continue recenter_on_axis(gn) trimmed.append(gn) op_variants[base] = trimmed # AMSa if 'ams' in fonts: for slot, (u, name) in sorted(U.AMSA.items()): import_glyph('ams', slot, name=name, unicode_=u) # Math alphabets. for tag, kind in [('bb', 'bb'), ('script', 'script'), ('frak', 'frak'), ('curly', 'curly')]: if tag not in fonts: continue for slot, (u, name) in sorted(U.alpha_map(kind).items()): import_glyph(tag, slot, name=name, unicode_=u) # Regular/Bold Roman donors supply upright Latin and mathematical bold. # MTPro2 supplies mathematical italic; mt2mb* is the separate mbf alphabet. def _donor_unicode_map(ffont): d = {} for g in ffont.glyphs(): u = g.unicode if u is not None and u >= 0: d[u] = g.glyphname return d def _copy_donor_glyph(ffont, byuni, src_u, dst_u, dst_name=None): src_name = byuni.get(src_u) if src_name is None: raise RuntimeError('Roman donor missing U+%04X' % src_u) if dst_u is not None and dst_u in uni_owner: return uni_owner[dst_u] gn = dst_name or uname(dst_u) ffont.selection.select(src_name) ffont.copy() out.createChar(dst_u if dst_u is not None else -1, gn) out.selection.select(gn) out.paste() scale = float(out.em) / float(ffont.em) if abs(scale - 1.0) > 1e-12: out[gn].transform(psMat.scale(scale)) out[gn].width = round(ffont[src_name].width * scale) if dst_u is not None: uni_owner[dst_u] = gn return gn ROMAN_REG = os.environ['MTP2_ROMAN_REGULAR'] ROMAN_BOLD = os.environ['MTP2_ROMAN_BOLD'] upright_dotless = None _nf_reg = fontforge.open(ROMAN_REG) _nr = _donor_unicode_map(_nf_reg) for _u in list(range(0x41, 0x5B)) + list(range(0x61, 0x7B)): _copy_donor_glyph(_nf_reg, _nr, _u, _u) # dotless i is used internally by dtls. Prefer the encoded U+0131 if present. if 0x0131 in _nr: upright_dotless = _copy_donor_glyph(_nf_reg, _nr, 0x0131, None, 'i.dotless') elif 'dotlessi' in _nf_reg: _nf_reg.selection.select('dotlessi') _nf_reg.copy() out.createChar(-1, 'i.dotless') out.selection.select('i.dotless') out.paste() _roman_reg_scale = float(out.em) / float(_nf_reg.em) if abs(_roman_reg_scale - 1.0) > 1e-12: out['i.dotless'].transform(psMat.scale(_roman_reg_scale)) out['i.dotless'].width = round(_nf_reg['dotlessi'].width * _roman_reg_scale) upright_dotless = 'i.dotless' _nf_reg.close() _nf_bold = fontforge.open(ROMAN_BOLD) _nb = _donor_unicode_map(_nf_bold) # Unicode Mathematical Bold Latin and digits follow \mathbf / rmdefault bold. for _i, _u in enumerate(range(0x41, 0x5B)): _copy_donor_glyph(_nf_bold, _nb, _u, 0x1D400 + _i) for _i, _u in enumerate(range(0x61, 0x7B)): _copy_donor_glyph(_nf_bold, _nb, _u, 0x1D41A + _i) for _i, _u in enumerate(range(0x30, 0x3A)): _copy_donor_glyph(_nf_bold, _nb, _u, 0x1D7CE + _i) _nf_bold.close() # Import MTPro2 bold math sources into the unified font. def alias_unicode(gname, u): old = uni_owner.get(u) if old is not None and old != gname: raise RuntimeError('Unicode U+%04X already owned by %s, cannot alias %s' % (u, old, gname)) g = out[gname] if g.unicode == -1: g.unicode = u elif g.unicode != u: vals = list(g.altuni or ()) if not any(a[0] == u for a in vals): vals.append((u, -1, 0)) g.altuni = tuple(vals) uni_owner[u] = gname return gname # Latin-shaped Greek aliases share glyph IDs, metrics, and substitutions. def _install_greek_latin_shaped_aliases(): groups = ( ('Basic Greek', U.GREEK_LATIN_SHAPED_BASIC_ALIASES), ('Mathematical Italic Greek', U.GREEK_LATIN_SHAPED_MATH_ITALIC_ALIASES), ) count = 0 for label, table in groups: for target_cp, source_cp in table.items(): source_glyph = uni_owner.get(source_cp) if source_glyph is None: raise RuntimeError('%s alias source U+%04X missing for U+%04X' % (label, source_cp, target_cp)) alias_unicode(source_glyph, target_cp) count += 1 if count != 30: raise RuntimeError('Greek Latin-shaped alias contract expected 30 entries, got %d' % count) _install_greek_latin_shaped_aliases() _MIT_CP_SLOT = {u: slot for slot, (u, _name) in U.MIT.items() if u is not None} _SYT_CP_SLOT = {u: slot for slot, (u, _name) in U.SYT.items() if u is not None} def bmit_cp(source_cp, target_cp, name=None): slot = _MIT_CP_SLOT.get(source_cp) if slot is None: raise RuntimeError('mt2bmit source mapping missing U+%04X' % source_cp) return import_glyph('bmit', slot, name=name, unicode_=target_cp) bold_it_latin = {} bold_dotless_i = None bold_dotless_j = None # Lite has donor bold Latin/digits, but no synthesized Full-only bold math. if 'bmit' in fonts and 'bsyt' in fonts: # Mathematical Bold Italic Latin U+1D468..U+1D49B. bold_it_latin = {} for i, ch in enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZ'): g = import_glyph('bmit', ord('A') + i, unicode_=0x1D468 + i) bold_it_latin[ch] = g for i, ch in enumerate('abcdefghijklmnopqrstuvwxyz'): g = import_glyph('bmit', ord('a') + i, unicode_=0x1D482 + i) bold_it_latin[ch] = g # Keep bold dotless forms unencoded; public dotless codepoints belong to italic. bold_dotless_i = import_glyph('bmit', 0x7B, name='u1D48A.dotless') bold_dotless_j = import_glyph('bmit', 0x7C, name='u1D48B.dotless') # Greek uppercase order in the Unicode Mathematical Alphanumeric block. greek_cap_names = [ 'ALPHA', 'BETA', 'GAMMA', 'DELTA', 'EPSILON', 'ZETA', 'ETA', 'THETA', 'IOTA', 'KAPPA', 'LAMDA', 'MU', 'NU', 'XI', 'OMICRON', 'PI', 'RHO', 'THETA SYMBOL', 'SIGMA', 'TAU', 'UPSILON', 'PHI', 'CHI', 'PSI', 'OMEGA'] basic_cap = { 'ALPHA': 0x0391, 'BETA': 0x0392, 'GAMMA': 0x0393, 'DELTA': 0x0394, 'EPSILON': 0x0395, 'ZETA': 0x0396, 'ETA': 0x0397, 'THETA': 0x0398, 'IOTA': 0x0399, 'KAPPA': 0x039A, 'LAMDA': 0x039B, 'MU': 0x039C, 'NU': 0x039D, 'XI': 0x039E, 'OMICRON': 0x039F, 'PI': 0x03A0, 'RHO': 0x03A1, 'SIGMA': 0x03A3, 'TAU': 0x03A4, 'UPSILON': 0x03A5, 'PHI': 0x03A6, 'CHI': 0x03A7, 'PSI': 0x03A8, 'OMEGA': 0x03A9} latin_visual = { 'ALPHA': 'A', 'BETA': 'B', 'EPSILON': 'E', 'ZETA': 'Z', 'ETA': 'H', 'IOTA': 'I', 'KAPPA': 'K', 'MU': 'M', 'NU': 'N', 'OMICRON': 'O', 'RHO': 'P', 'TAU': 'T', 'CHI': 'X'} # Use mt2bmit for non-Latin-shaped bold Greek and Latin aliases for the rest. bold_greek = {} for idx, name in enumerate(greek_cap_names): tgt = 0x1D6A8 + idx if name == 'THETA SYMBOL': g = bold_greek['THETA'] alias_unicode(g, tgt) elif basic_cap[name] in _MIT_CP_SLOT: g = bmit_cp(basic_cap[name], tgt) else: g = uni_owner[0x1D400 + (ord(latin_visual[name]) - ord('A'))] alias_unicode(g, tgt) bold_greek[name] = g # Genuine bold nabla from mt2bsyt; upright and italic mathematical nabla share it. _bsnabla_slot = _SYT_CP_SLOT.get(0x2207) if _bsnabla_slot is None: raise RuntimeError('mt2bsyt nabla source missing') bold_nabla = import_glyph('bsyt', _bsnabla_slot, unicode_=0x1D6C1) alias_unicode(bold_nabla, 0x1D735) small_order = [ ('ALPHA', 0x03B1), ('BETA', 0x03B2), ('GAMMA', 0x03B3), ('DELTA', 0x03B4), ('EPSILON', 0x03B5), ('ZETA', 0x03B6), ('ETA', 0x03B7), ('THETA', 0x03B8), ('IOTA', 0x03B9), ('KAPPA', 0x03BA), ('LAMDA', 0x03BB), ('MU', 0x03BC), ('NU', 0x03BD), ('XI', 0x03BE), ('OMICRON', 0x03BF), ('PI', 0x03C0), ('RHO', 0x03C1), ('FINAL SIGMA', 0x03C2), ('SIGMA', 0x03C3), ('TAU', 0x03C4), ('UPSILON', 0x03C5), ('PHI', 0x03C6), ('CHI', 0x03C7), ('PSI', 0x03C8), ('OMEGA', 0x03C9)] for idx, (_name, source_cp) in enumerate(small_order): tgt = 0x1D6C2 + idx if source_cp in _MIT_CP_SLOT: bmit_cp(source_cp, tgt) else: # Greek omicron has the Latin o design. alias_unicode(uni_owner[0x1D41A + (ord('o') - ord('a'))], tgt) # Bold partial differential and upright variant Greek symbols. bold_partial = bmit_cp(0x1D715, 0x1D6DB) for i, source_cp in enumerate((0x03F5, 0x03D1, 0x03F0, 0x03D5, 0x03F1, 0x03D6)): bmit_cp(source_cp, 0x1D6DC + i) # Mathematical Bold Italic Greek U+1D71C..U+1D755. italic_cap_regular = { 'GAMMA': 0x1D6E4, 'DELTA': 0x1D6E5, 'THETA': 0x1D6E9, 'LAMDA': 0x1D6EC, 'XI': 0x1D6EF, 'PI': 0x1D6F1, 'SIGMA': 0x1D6F4, 'UPSILON': 0x1D6F6, 'PHI': 0x1D6F7, 'PSI': 0x1D6F9, 'OMEGA': 0x1D6FA} bold_it_greek = {} for idx, name in enumerate(greek_cap_names): tgt = 0x1D71C + idx if name == 'THETA SYMBOL': g = bold_it_greek['THETA'] alias_unicode(g, tgt) elif name in italic_cap_regular: g = bmit_cp(italic_cap_regular[name], tgt) else: g = bold_it_latin[latin_visual[name]] alias_unicode(g, tgt) bold_it_greek[name] = g reg_it_small = [ 0x1D6FC, 0x1D6FD, 0x1D6FE, 0x1D6FF, 0x1D700, 0x1D701, 0x1D702, 0x1D703, 0x1D704, 0x1D705, 0x1D706, 0x1D707, 0x1D708, 0x1D709, 0x1D70A, 0x1D70B, 0x1D70C, 0x1D70D, 0x1D70E, 0x1D70F, 0x1D710, 0x1D711, 0x1D712, 0x1D713, 0x1D714] for i, source_cp in enumerate(reg_it_small): tgt = 0x1D736 + i if source_cp in _MIT_CP_SLOT: bmit_cp(source_cp, tgt) else: # mathematical italic omicron = Latin italic o alias_unicode(bold_it_latin['o'], tgt) alias_unicode(bold_partial, 0x1D74F) for i, source_cp in enumerate((0x1D716, 0x1D717, 0x1D718, 0x1D719, 0x1D71A, 0x1D71B)): bmit_cp(source_cp, 0x1D750 + i) # Vertical delimiter variants and assemblies. def exa_chain(start): """Return the NEXTLARGER chain and its terminal VARCHAR recipe.""" _, tfm, _ = fonts['exa'] seq, c, seen = [], start, set() while c is not None and c in tfm.chars and c not in seen: seen.add(c) seq.append(c) vc = tfm.chars[c]['varchar'] nxt = tfm.chars[c]['next'] if nxt is None: return seq, vc c = nxt return seq, None MINOVL = None # Assigned from MathConstants below. vert_variants = {} # base gname -> [gnames] vert_assembly = {} # base -> [(glyph, extender)], bottom to top # Cap generic connector estimates with local source geometry where needed. connector_limits = {} piece_names = dict(U.EXA_PIECES) def piece(slot): nm = piece_names.get(slot, f'exa.piece{slot}') return import_glyph('exa', slot, name=nm) def build_vdelim(basespec, exastart, base_uni=None, base_name=None): tag, slot = basespec base = import_glyph(tag, slot, name=base_name, unicode_=base_uni) if base is None or 'exa' not in fonts: return _, tfm, _ = fonts['exa'] seq, vc = exa_chain(exastart) var = [base] for c in seq: # A TeX VARCHAR slot selects an assembly, not a fixed-size variant. is_terminal_piece = bool(tfm.chars.get(c, {}).get('varchar')) or ( vc is not None and c == seq[-1] and c in (vc.get('TOP'), vc.get('BOT'), vc.get('MID')) and c != vc.get('REP')) if is_terminal_piece and len(seq) > 1: continue # Exclude assembly parts from fixed-size variants. v = import_glyph('exa', c, name=f'{base}.v{len(var)}') if v: var.append(v) vert_variants[base] = var if vc and 'REP' in vc: parts = [] # Bottom to top. if 'BOT' in vc: parts.append((piece(vc['BOT']), 0)) parts.append((piece(vc['REP']), 1)) if 'MID' in vc: parts.append((piece(vc['MID']), 0)) parts.append((piece(vc['REP']), 1)) if 'TOP' in vc: parts.append((piece(vc['TOP']), 0)) parts = [(p, e) for (p, e) in parts if p] if parts and not any(e == 0 for (p, e) in parts): # Repeat-only recipes use the same glyph as fixed ends and a central extender. rep = parts[0][0] parts = [(rep, 0), (rep, 1), (rep, 0)] if parts: vert_assembly[base] = parts for u, (bspec, ex0) in U.V_DELIMS.items(): build_vdelim(bspec, ex0, base_uni=u) for nm, (bspec, ex0) in U.V_DELIMS_ALT.items(): build_vdelim(bspec, ex0, base_name=nm) # Include operator variants. vert_variants.update(op_variants) # Extend variant ladders from the package-selected extension families. EXT_TAGS = ('exe', 'exf', 'exg') def chain_slots(tfm, start): """Follow the TFM NEXTLARGER chain.""" out, seen, s = [], set(), start while s is not None and s in tfm.chars and s not in seen: seen.add(s) out.append(s) s = tfm.chars[s].get('next') return out def chain_heads(tfm): """Return slots with no incoming NEXTLARGER link.""" targets = {c['next'] for c in tfm.chars.values() if c.get('next') is not None} return {s for s in tfm.chars if s not in targets} def extend_chain(base_gname, slot, table, size_key): """Append an extension chain only when its head matches the base source.""" added = [] for tag in EXT_TAGS: if tag not in fonts: continue _, tfm, _ = fonts[tag] if slot not in tfm.chars or slot not in chain_heads(tfm): continue for s in chain_slots(tfm, slot): g = import_glyph(tag, s, name=f'{base_gname}.{tag}{s}') if not g: continue bb = out[g].boundingBox() # Compare height for vertical variants and advance for horizontal variants. if size_key == 'h': extent, cap = bb[3] - bb[1], MAXVAR else: extent, cap = out[g].width, MAXWIDTH if cap and extent > cap: # Discard oversized variants and their import records. imported.pop((tag, s), None) out.removeGlyph(out[g]) continue if size_key == 'h': recenter_on_axis(g) added.append(g) if added: table[base_gname] = table.get(base_gname, [base_gname]) + added # Sort variants by size; equal-size stages may remain. key = (lambda gn: out[gn].width) if size_key == 'w' else \ (lambda gn: out[gn].boundingBox()[3] - out[gn].boundingBox()[1]) head, rest = table[base_gname][0], table[base_gname][1:] table[base_gname] = [head] + sorted(rest, key=key) return added # Only extend matching chain heads; brackets, floors, and ceilings have none. for u, (bspec, ex0) in U.V_DELIMS.items(): if u not in uni_owner: continue base = uni_owner[u] extend_chain(base, ex0, vert_variants, 'h') # Horizontal variants and brace assemblies. horiz_variants = {} for u, chain in U.H_CHAINS.items(): gnames = [] for tag, slot in chain: if tag not in fonts: continue g = import_glyph(tag, slot, unicode_=u if not gnames else None, name=None if not gnames else None) if g: gnames.append(g) if len(gnames) >= 1: horiz_variants[gnames[0]] = gnames # Keep public bar accents fixed. Private variants do not remap standard commands. _bar_base = uni_owner.get(0x0304) if _bar_base is not None: _bar_chain = list(horiz_variants.get(_bar_base, [_bar_base])) if len(_bar_chain) > 1: _widebar_cp = 0xE286 _widebar_name = 'wideoverbar.compat' if _widebar_name not in out: out.createChar(_widebar_cp, _widebar_name) _wg = out[_widebar_name] _wg.addReference(_bar_base) # The private finite bar keeps its advance and outline; attachment is width/2. _wg.width = out[_bar_base].width _wg.unlinkRef() uni_owner[_widebar_cp] = _widebar_name horiz_variants[_widebar_name] = [_widebar_name] + _bar_chain[1:] horiz_variants.pop(_bar_base, None) # Extend only matching wide-accent chains. Equal slots can have different semantics. _WIDE_ACCENT_EXTENSION = (0x0302, 0x0303, 0x030C) for u in _WIDE_ACCENT_EXTENSION: chain = U.H_CHAINS.get(u, ()) if u not in uni_owner: continue exa_slots = [slot for (tag, slot) in chain if tag == 'exa'] if exa_slots: extend_chain(uni_owner[u], exa_slots[0], horiz_variants, 'w') # Use the designated mt2exf overparen continuation at its source scale. if 0x23DC in uni_owner and 'exf' in fonts: base = uni_owner[0x23DC] extra = [] for slot in range(177, 184): g = import_glyph('exf', slot, name=f'{base}.exf{slot}') if not g: continue if MAXWIDTH and out[g].width > MAXWIDTH: imported.pop(('exf', slot), None) out.removeGlyph(out[g]) continue extra.append(g) if extra: vals = horiz_variants.get(base, [base]) + extra # Deduplicate equal-width stages while preserving the first source form. seen_w = set() dedup = [] for gn in sorted(vals, key=lambda q: out[q].width): w = out[gn].width if w in seen_w: continue seen_w.add(w) dedup.append(gn) horiz_variants[base] = dedup # Bridge the fixed overparen ladders with an assembly derived from the local outline. def _make_overparen_parts(): left = 'overparen.left.mt2' ext = 'overparen.ext.mt2' right = 'overparen.right.mt2' try: tpl = _SOURCE_SNAPSHOT['geometry']['overparen_template'] ops = tpl['path'] except Exception as exc: raise RuntimeError('local overparen source geometry is missing') from exc expected_ops = ('moveTo', 'lineTo', 'curveTo', 'curveTo', 'lineTo', 'curveTo', 'curveTo', 'closePath') if tuple(rec.get('op') for rec in ops) != expected_ops: raise RuntimeError('unexpected local MTPro2 overparen contour topology') def ipoints(i): pts = [] for x, y in ops[i]['points']: if round(x) != x or round(y) != y: raise RuntimeError('non-integral local Type1 overparen coordinate') pts.append((int(x), int(y))) return pts start_pt = ipoints(0)[0] left_inner = ipoints(1)[0] lower_left = ipoints(2) lower_right = ipoints(3) right_edge = ipoints(4)[0] upper_right = ipoints(5) upper_left = ipoints(6) mid_lower = lower_left[-1] mid_upper = upper_right[-1] if mid_lower[0] != mid_upper[0] or start_pt != upper_left[-1]: raise RuntimeError( 'local overparen source contour is not the expected symmetric-cap topology') span = right_edge[0] - start_pt[0] if span <= 0: raise RuntimeError('invalid local overparen source span') # Derive connector proportions from the local span. connector = round(span / 15) ext_width = round(span / 6) inner_x = mid_lower[0] + connector right_dx = connector - mid_lower[0] left_width = inner_x - start_pt[0] right_width = right_edge[0] + right_dx if left not in out: out.createChar(-1, left) g = out[left] pen = g.glyphPen() pen.moveTo(start_pt) pen.lineTo(left_inner) pen.curveTo(*lower_left) pen.lineTo((inner_x, mid_lower[1])) pen.lineTo((inner_x, mid_upper[1])) pen.lineTo(mid_upper) pen.curveTo(*upper_left) pen.closePath() pen = None g.width = left_width if ext not in out: out.createChar(-1, ext) g = out[ext] pen = g.glyphPen() pen.moveTo((0, mid_lower[1])) pen.lineTo((ext_width, mid_lower[1])) pen.lineTo((ext_width, mid_upper[1])) pen.lineTo((0, mid_upper[1])) pen.closePath() pen = None g.width = ext_width if right not in out: def tx(pt): return (pt[0] + right_dx, pt[1]) out.createChar(-1, right) g = out[right] pen = g.glyphPen() pen.moveTo((0, mid_lower[1])) pen.lineTo((connector, mid_lower[1])) pen.curveTo(*(tx(q) for q in lower_right)) pen.lineTo(tx(right_edge)) pen.curveTo(*(tx(q) for q in upper_right)) pen.lineTo((0, mid_upper[1])) pen.closePath() pen = None g.width = right_width return left, ext, right _opl, _ope, _opr = _make_overparen_parts() vert_assembly[('H', base)] = [(_opl, 0), (_ope, 1), (_opr, 0)] # Normalize combining accents: zero advance, left-shifted ink, TopAccent alignment. _NORMALIZED_ACCENT_MACROS = ( 'grave', 'acute', 'check', 'breve', 'bar', 'hat', 'dot', 'tilde', 'ddot', 'mathring', 'vec', 'dotup', 'ddotup', 'dddot', 'ddddot', 'dddotup', 'ddddotup', ) try: ACCENT_SLOTS = [SOURCE_POLICY.accents[name].slot for name in _NORMALIZED_ACCENT_MACROS] except KeyError as exc: raise RuntimeError('required local MTPro2 accent declaration missing: %s' % exc.args[0]) accent_glyphs = set() ACC_CENTER = -(_axis_height() + round((fonts['exa'][1].fontdimen[8] * out.em) / 4)) for slot in ACCENT_SLOTS: key = ('syt', slot) if key not in imported: continue g = out[imported[key]] bb = g.boundingBox() cx = (bb[0] + bb[2]) / 2 g.transform(psMat.translate(ACC_CENTER - cx, 0)) g.width = 0 accent_glyphs.add(g.glyphname) # Dot and prime geometry. def compose(uni, name, ref, offsets, adv): """Compose copies of a reference glyph at the given offsets.""" gn = uname(uni) if uni is not None else name if uni is not None and uni in uni_owner: return None out.createChar(uni if uni is not None else -1, gn) g = out[gn] for (dx, dy) in offsets: g.addReference(ref, psMat.translate(dx, dy)) g.width = round(adv) if uni is not None: uni_owner[uni] = gn return gn if ('mit', 0x3A) in imported and ('syt', 0x01) in imported: # Read dot geometry from the local package; horizontal spacing uses math units. P = out[imported[('mit', 0x3A)]].width Cw = out[imported[('syt', 0x01)]].width per = imported[('mit', 0x3A)] cdo = imported[('syt', 0x01)] tfm_symbols = fonts['syt'][1] ellipsis_gap = _mu_to_units(Fraction(3, 1)) compose(0x2026, None, per, [(0, 0), (P + ellipsis_gap, 0), (2 * (P + ellipsis_gap), 0)], 3 * P + 2 * ellipsis_gap) compose(0x22EF, None, cdo, [(0, 0), (Cw + ellipsis_gap, 0), (2 * (Cw + ellipsis_gap), 0)], 3 * Cw + 2 * ellipsis_gap) vstep = _pt_to_units(SOURCE_POLICY.dot_macros.vdots_step_pt, tfm_symbols) compose(0x22EE, None, per, [(0, 0), (0, vstep), (0, 2 * vstep)], P) diagonal_gap = _mu_to_units(SOURCE_POLICY.dot_macros.ddots_inner_mu) raises = tuple(_pt_to_units(x, tfm_symbols) for x in SOURCE_POLICY.dot_macros.ddots_raises_pt) if len(raises) != 3: raise RuntimeError('local ddots policy must contain three raises') xs = [0, P + diagonal_gap, 2 * (P + diagonal_gap)] dadv = 3 * P + 2 * diagonal_gap compose(0x22F1, None, per, list(zip(xs, raises)), dadv) compose(0x22F0, None, per, list(zip(xs, reversed(raises))), dadv) # Center standalone dot glyphs on the local math axis. axis = _axis_height() for u in (0x22EE, 0x22F1, 0x22F0): if u not in uni_owner: continue g = out[uni_owner[u]] bb = g.boundingBox() g.transform(psMat.translate(0, axis - (bb[1] + bb[3]) / 2)) # Compose Word-compatible primes after importing optical script forms. # Horizontal arrow assemblies use internal parts with trimmed connector bearings. # Public outlines and advances remain unchanged. ARROW_CONNECTOR = 200 def _trim_hconnector(src, suffix, connect_left=False, connect_right=False): """Create an unencoded assembly-only copy with connector sidebearings removed.""" bits = ('L' if connect_left else '') + ('R' if connect_right else '') nm = f'{src}.hconn{bits}.{suffix}' if nm in out: return nm out.createChar(-1, nm) g = out[nm] g.addReference(src) g.unlinkRef() bb = g.boundingBox() xmin, _, xmax, _ = bb oldw = out[src].width if connect_left: g.transform(psMat.translate(-xmin, 0)) oldw -= xmin xmax -= xmin xmin = 0 if connect_right: # The right connector ends at actual ink, not at the source advance edge. g.width = round(xmax) else: g.width = round(oldw) if connect_left and connect_right: bb = g.boundingBox() if bb[0] != 0: g.transform(psMat.translate(-bb[0], 0)) bb = g.boundingBox() g.width = max(1, round(bb[2])) return nm def _resolve_harrow_ref(ref): if ref == 'Relbar': return imported.get(('syt', 0x48)) if isinstance(ref, int): return uni_owner.get(ref) return None def _arrow_component(src, idx, count, extender, suffix): left = idx > 0 right = idx < count - 1 gn = _trim_hconnector(src, suffix, left, right) if extender: # Both edges are straight connector material after trimming. conn = min(ARROW_CONNECTOR, max(100, out[gn].width // 3)) return gn, 1, conn, conn, out[gn].width conn = min(ARROW_CONNECTOR, max(100, out[gn].width // 3)) return gn, 0, (conn if left else 0), (conn if right else 0), out[gn].width def _resolve_arrow_parts(part_spec, suffix): raw = [] for kind, ref in part_spec: gn = _resolve_harrow_ref(ref) if gn is None: return None raw.append((gn, kind == 'E')) return [_arrow_component(gn, i, len(raw), ext, suffix) for i, (gn, ext) in enumerate(raw)] def harrow_assembly(base_uni, part_spec): if base_uni not in uni_owner: return tups = _resolve_arrow_parts(part_spec, f'U{base_uni:04X}') if tups: out[uni_owner[base_uni]].horizontalComponents = tuple(tups) # Arrow fills use fixed ends and an extender from the MTPro2 outlines. _ARROW_SPECS = { 0x2190: [('G', 0x2190), ('E', 0x2212), ('G', 0x2212)], 0x2192: [('G', 0x2212), ('E', 0x2212), ('G', 0x2192)], 0x2194: [('G', 0x2190), ('E', 0x2212), ('G', 0x2192)], 0x21D0: [('G', 0x21D0), ('E', 'Relbar'), ('G', 'Relbar')], 0x21D2: [('G', 'Relbar'), ('E', 'Relbar'), ('G', 0x21D2)], 0x21D4: [('G', 0x21D0), ('E', 'Relbar'), ('G', 0x21D2)], } for _u, _spec in _ARROW_SPECS.items(): harrow_assembly(_u, _spec) # Mapsto uses dedicated short/long source glyphs, not an arbitrary-width assembly. if 0x20D7 in uni_owner and 0x2192 in uni_owner and 0x2212 in uni_owner: vb = out[uni_owner[0x20D7]].boundingBox() ab = out[uni_owner[0x2192]].boundingBox() dy = round((vb[1] + vb[3]) / 2 - (ab[1] + ab[3]) / 2) + 25 def accent_copy(src, suffix): nm = f'{src}.{suffix}' if nm in out: return nm out.createChar(-1, nm) g = out[nm] g.addReference(src, psMat.translate(0, dy)) g.width = out[src].width g.unlinkRef() return nm if 0x20D6 not in uni_owner: src = uni_owner[0x20D7] nm = uname(0x20D6) out.createChar(0x20D6, nm) g = out[nm] g.addReference(src) g.unlinkRef() g.transform(psMat.scale(-1, 1)) bb = g.boundingBox() g.transform(psMat.translate(vb[0] - bb[0], 0)) g.width = 0 uni_owner[0x20D6] = nm accent_glyphs.add(nm) shaft_src = uni_owner[0x2212] shaft = _trim_hconnector(shaft_src, 'vector.ex', True, True) shaft_acc = accent_copy(shaft, 'acc') shaft_conn = min(ARROW_CONNECTOR, max(100, out[shaft].width // 3)) # Keep public U+20D7 fixed; private U+E287 carries the accent-height assembly. # Standard commands are not remapped to the private adapter. _widevec_cp = 0xE287 _widevec_name = 'widevector.compat' if _widevec_name not in out: out.createChar(_widevec_cp, _widevec_name) _wg = out[_widevec_name] _wg.addReference(uni_owner[0x20D7]) _wg.width = out[uni_owner[0x20D7]].width _wg.unlinkRef() uni_owner[_widevec_cp] = _widevec_name accent_glyphs.add(_widevec_name) # The right vector starts with its endpoint; shaft_acc supplies extra width. rend = accent_copy(uni_owner[0x2192], 'vector.end.acc') rconn = min(ARROW_CONNECTOR, max(100, out[rend].width // 3)) out[_widevec_name].horizontalComponents = ( (shaft_acc, 1, shaft_conn, shaft_conn, out[shaft_acc].width), (rend, 0, rconn, 0, out[rend].width), ) # Left vector: same minimum width, mirrored assembly order. lend = accent_copy(uni_owner[0x2190], 'vector.end.acc') lconn = min(ARROW_CONNECTOR, max(100, out[lend].width // 3)) out[uni_owner[0x20D6]].horizontalComponents = ( (lend, 0, 0, lconn, out[lend].width), (shaft_acc, 1, shaft_conn, shaft_conn, out[shaft_acc].width), ) def make_rule(name, y0, y1): out.createChar(-1, name) g = out[name] pen = g.glyphPen() w = 200 pen.moveTo((0, y0)) pen.lineTo((0, y1)) pen.lineTo((w, y1)) pen.lineTo((w, y0)) pen.closePath() pen = None g.width = w return name if 'exa' in fonts: _, exatfm, _ = fonts['exa'] bld, brd, blu, bru = (piece(130), piece(131), piece(132), piece(133)) if all([bld, brd, blu, bru]): # Standard braces use mt2exa corners and rules, separate from overcbrace designs. # Read the rule band from the local braceld TFM height. brace_rule_h = round(exatfm.chars[130]['ht'] * 1000) ext = make_rule('brace.hext', 0, brace_rule_h) def merge_hparts(left, right, name): if name in out: return name out.createChar(-1, name) g = out[name] wl = out[left].width g.addReference(left) g.addReference(right, psMat.translate(wl, 0)) g.width = wl + out[right].width g.unlinkRef() return name # Merge cusp halves while preserving their source outlines. over_center = merge_hparts(bru, blu, 'brace.over.center') under_center = merge_hparts(brd, bld, 'brace.under.center') def shifted_copy(src, name, dy): if name in out: return name out.createChar(-1, name) g = out[name] g.addReference(src, psMat.translate(0, dy)) g.width = out[src].width g.unlinkRef() return name # Set the top-accent origin using OpenType/Word conversion policy. _OVERBRACE_TARGET_YMIN = 539 # OpenType/Word migration policy over_ymin = min(out[q].boundingBox()[1] for q in (bld, ext, over_center, brd)) over_dy = round(_OVERBRACE_TARGET_YMIN - over_ymin) over_left = shifted_copy(bld, 'brace.over.left', over_dy) over_ext = shifted_copy(ext, 'brace.over.ext', over_dy) over_center_shift = shifted_copy(over_center, 'brace.over.center.shift', over_dy) over_right = shifted_copy(brd, 'brace.over.right', over_dy) # Cap curved-part connectors at the local straight rule band minus tolerance. # Extenders expose their full rule width. brace_fixed_connector = max( 0, brace_rule_h - CONNECTOR_PROFILE_TOLERANCE) for q in (bld, brd, blu, bru, over_left, over_center_shift, over_right, over_center, under_center): connector_limits[q] = brace_fixed_connector connector_limits[ext] = out[ext].width connector_limits[over_ext] = out[over_ext].width brace_extenders = {ext, over_ext} for u, parts in [ (0x23DE, [over_left, over_ext, over_center_shift, over_ext, over_right]), (0x23DF, [blu, ext, under_center, ext, bru])]: gn = uname(u) if gn not in out: out.createChar(u, gn) g = out[gn] # Build a small fallback; the assembly handles arbitrary widths. fixed = [q for q in parts if q != ext] x = 0 for q in fixed: g.addReference(q, psMat.translate(x, 0)) x += out[q].width g.width = x uni_owner[u] = gn horiz_variants[gn] = [gn] vert_assembly[('H', gn)] = [ (q, 1 if q in brace_extenders else 0) for q in parts] # TopAccent uses local family skewchar declarations; None means no skewchar. SKEW = SOURCE_SKEW topaccent = {} for tag in tuple(SOURCE_SKEW): if tag not in fonts: continue _, tfm, _ = fonts[tag] sk = SKEW[tag] for (tg, slot), gname in list(imported.items()): if tg != tag or slot not in tfm.chars: continue w = tfm.chars[slot]['wd'] ic = tfm.chars[slot].get('ic') or 0.0 kern = tfm.kerns.get((slot, sk), 0.0) if sk is not None else 0.0 # TeX centers over the accentee width plus CHARIC, then adds skew kern. topaccent[gname] = round(((w + ic) / 2 + kern) * 1000) # Combining accents attach at the ink center. for gname in accent_glyphs: topaccent[gname] = ACC_CENTER # Other glyphs attach at the advance center. for gname in list(imported.values()): if gname not in topaccent: g = out[gname] topaccent[gname] = g.width // 2 # MATH constants. sy_fd = fonts['syt'][1].fontdimen ex_fd = fonts['exa'][1].fontdimen # Derive the display threshold from the local text/display summation sizes. _op_text = fonts['exa'][1].chars.get(0x50) _op_display = fonts['exa'][1].chars.get(0x58) if not _op_text or not _op_display: raise SystemExit('local extension TFM lacks the summation size pair required for MATH policy') _op_text_size = round((_op_text['ht'] + _op_text['dp']) * out.em) _op_display_size = round((_op_display['ht'] + _op_display['dp']) * out.em) _display_operator_min_height = (_op_text_size + _op_display_size) // 2 C = math_constants.compute( sy_fd, ex_fd, script_sy=fonts['syt_s'][1].fontdimen, script_ratio=SCRIPT_RATIO, scriptscript_ratio=SCRIPTSCRIPT_RATIO, display_operator_min_height=_display_operator_min_height, quad=out.em, ) ALL_MATH_SCALARS = dict(C) MINOVL = C.pop('MinConnectorOverlap') FF_NAMES = {'FractionNumDisplayStyleGapMin': 'FractionNumeratorDisplayStyleGapMin', 'FractionDenomDisplayStyleGapMin': 'FractionDenominatorDisplayStyleGapMin'} for k, v in C.items(): try: setattr(out.math, FF_NAMES.get(k, k), int(v)) except Exception as exc: print('math const fail', k, exc) out.math.MinConnectorOverlap = MINOVL # Per-glyph MATH data. for gname, ta in topaccent.items(): try: out[gname].topaccent = ta except Exception: pass def glyph_size(gname): bb = out[gname].boundingBox() return bb[3] - bb[1] for base, var in vert_variants.items(): var2 = sorted(dict.fromkeys(var), key=glyph_size) if len(var2) > 1 or base in vert_assembly: out[base].verticalVariants = ' '.join(var2) for base, var in horiz_variants.items(): var2 = sorted(dict.fromkeys(var), key=lambda g: out[g].width) if len(var2) > 1: out[base].horizontalVariants = ' '.join(var2) # Measure stable connector runs from flattened outline cross-sections. # TFM recipes provide no overlap lengths. def _flatten(gname, steps=8): polys = [] for contour in out[gname].foreground: pts = [] seq = [(p.x, p.y, p.on_curve) for p in contour] if not seq: continue seq.append(seq[0]) i = 0 while i < len(seq) - 1: x0, y0, _ = seq[i] x1, y1, on1 = seq[i + 1] if on1: pts.append((x1, y1)) i += 1 else: ctrl = [] j = i + 1 while j < len(seq) and not seq[j][2]: ctrl.append(seq[j][:2]) j += 1 if j >= len(seq): break x3, y3, _ = seq[j] for k in range(1, steps + 1): t = k / steps if len(ctrl) == 1: cx, cy = ctrl[0] xx = (1 - t)**2 * x0 + 2 * (1 - t) * t * cx + t * t * x3 yy = (1 - t)**2 * y0 + 2 * (1 - t) * t * cy + t * t * y3 else: c1, c2 = ctrl[0], ctrl[-1] xx = ((1 - t)**3 * x0 + 3 * (1 - t)**2 * t * \ c1[0] + 3 * (1 - t) * t * t * c2[0] + t**3 * x3) yy = ((1 - t)**3 * y0 + 3 * (1 - t)**2 * t * \ c1[1] + 3 * (1 - t) * t * t * c2[1] + t**3 * y3) pts.append((xx, yy)) i = j if pts: polys.append(pts) return polys def _profile(gname, vertical, n=64): polys = _flatten(gname) if not polys: return [] bb = out[gname].boundingBox() lo, hi = (bb[1], bb[3]) if vertical else (bb[0], bb[2]) if hi - lo <= 0: return [] res = [] for k in range(n): v = lo + (hi - lo) * (k + 0.5) / n xs = [] for pts in polys: m = len(pts) for a in range(m): p, q = pts[a], pts[(a + 1) % m] pa, qa = (p[1], q[1]) if vertical else (p[0], q[0]) if (pa <= v < qa) or (qa <= v < pa): t = (v - pa) / (qa - pa) pb, qb = (p[0], q[0]) if vertical else (p[1], q[1]) xs.append(pb + (qb - pb) * t) res.append((min(xs), max(xs)) if len(xs) >= 2 else None) return res def connector_run(gname, vertical, at_end, tol=CONNECTOR_PROFILE_TOLERANCE): prof = _profile(gname, vertical) if not prof: return 0 bb = out[gname].boundingBox() span = (bb[3] - bb[1]) if vertical else (bb[2] - bb[0]) step = span / len(prof) seq = list(reversed(prof)) if at_end else list(prof) ref = seq[0] if ref is None: return 0 run = 0 for cur in seq: if cur is None or abs(cur[0] - ref[0]) > tol or abs(cur[1] - ref[1]) > tol: break run += 1 length = max(0, int(run * step)) limit = connector_limits.get(gname) return min(length, limit) if limit is not None else length def _norm_vpart(p): """Align vertical assembly parts to their ink bottom.""" nm = f'{p}.vn' if nm not in out: bb = out[p].boundingBox() out.createChar(-1, nm) g = out[nm] g.addReference(p, psMat.translate(0, -bb[1])) g.width = out[p].width g.unlinkRef() return nm def part_tuple(gname, is_ext, vertical=True): g = out[gname] if vertical: bb = g.boundingBox() full = round(bb[3] - bb[1]) else: full = g.width start = connector_run(gname, vertical, False) end = connector_run(gname, vertical, True) return (gname, is_ext, start, end, full) _runs = [] for _b, _p in vert_assembly.items(): n = len(_p) vert = not isinstance(_b, tuple) for i, (p, e) in enumerate(_p): if i or e: _runs.append(connector_run(p, vert, False)) if i < n - 1 or e: _runs.append(connector_run(p, vert, True)) _ov = int(getattr(out.math, 'MinConnectorOverlap', 100) or 100) if _runs and min(_runs) < _ov: out.math.MinConnectorOverlap = max(0, int(min(_runs))) for base, parts in vert_assembly.items(): if isinstance(base, tuple): # Horizontal assemblies. _, gname = base # Only joining edges have connectors; outside fixed edges must have zero length. tups = [] n = len(parts) for i, (p, e) in enumerate(parts): q = list(part_tuple(p, e, vertical=False)) if i == 0 and not e: q[2] = 0 # StartConnectorLength if i == n - 1 and not e: q[3] = 0 # EndConnectorLength tups.append(tuple(q)) out[gname].horizontalComponents = tuple(tups) else: # Extenders need both connectors even at an assembly boundary. parts = [(_norm_vpart(p), e) for (p, e) in parts] tups = [] n = len(parts) for i, (p, e) in enumerate(parts): bb = out[p].boundingBox() full = round(bb[3] - bb[1]) start = 0 if i == 0 and not e else connector_run(p, True, False) end = 0 if i == n - 1 and not e else connector_run(p, True, True) tups.append((p, e, start, end, full)) out[base].verticalComponents = tuple(tups) # Extended shapes include larger variants, assembly parts, and display operators. extended = set() for base, var in vert_variants.items(): extended.update(var[1:]) for base, parts in vert_assembly.items(): if not isinstance(base, tuple): extended.update(p for (p, _) in parts) # Mark the base integral family ExtendedShape for OpenType script/limit placement. for _u in range(0x222B, 0x2234): if _u in uni_owner: extended.add(uni_owner[_u]) for gname in extended: try: out[gname].isExtendedShape = True except Exception: pass # Optical script-size substitutions. ssty_map = {} def add_ssty(coretag, stag, sstag, table): if stag not in fonts: return for slot in table: key = (coretag, slot) if key not in imported: continue base = imported[key] alts = [] for lvl, t in ((1, stag), (2, sstag)): if t not in fonts: continue g = import_glyph(t, slot, name=f'{base}.ssty{lvl}') if g: alts.append(g) if alts: ssty_map[base] = alts add_ssty('mit', 'mit_s', 'mit_ss', U.MIT) add_ssty('syt', 'syt_s', 'syt_ss', U.SYT) add_ssty('ams', 'ams_s', 'ams_ss', U.AMSA) add_ssty('bb', 'bb_s', 'bb_ss', U.alpha_map('bb')) # Import option-family script sources so cv03..cv07 preserve optical designs. for _core, _s, _ss in ( ('bbd', 'bbd_s', 'bbd_ss'), ('bbi', 'bbi_s', 'bbi_ss'), ('hrb', 'hrb_s', 'hrb_ss'), ('hrbd', 'hrbd_s', 'hrbd_ss'), ('hbi', 'hbi_s', 'hbi_ss')): if _core not in fonts: continue for _slot, (_u, _nm) in U.alpha_map('bb').items(): _base = import_glyph(_core, _slot, name=f'{_nm}.{_core}') if not _base: continue _alts = [] for _lvl, _tag in ((1, _s), (2, _ss)): if _tag not in fonts: continue _g = import_glyph(_tag, _slot, name=f'{_base}.ssty{_lvl}') if _g: _alts.append(_g) if _alts: ssty_map[_base] = _alts add_ssty('script', 'script_s', 'script_ss', U.alpha_map('script')) add_ssty('frak', 'frak_s', 'frak_ss', U.alpha_map('frak')) # Donor mathematical bold has no mt2mb* script substitutions; mbf is separate. add_ssty('curly', 'curly_s', 'curly_ss', U.alpha_map('curly')) add_ssty('bmit', 'bmit_s', 'bmit_ss', U.MIT) add_ssty('bsyt', 'bsyt_s', 'bsyt_ss', U.SYT) # Normalize script accents and use their own source-family skew kern. for tag in ('syt_s', 'syt_ss'): for slot in ACCENT_SLOTS: key = (tag, slot) if key not in imported: continue g = out[imported[key]] bb = g.boundingBox() g.transform(psMat.translate(ACC_CENTER - (bb[0] + bb[2]) / 2, 0)) g.width = 0 g.topaccent = ACC_CENTER accent_glyphs.add(g.glyphname) for tag in ('mit_s', 'mit_ss', 'syt_s', 'syt_ss', 'bmit_s', 'bmit_ss', 'bsyt_s', 'bsyt_ss', 'bb_s', 'bb_ss', 'bbi_s', 'bbi_ss', 'bbd_s', 'bbd_ss', 'hrb_s', 'hrb_ss', 'hrbd_s', 'hrbd_ss', 'hbi_s', 'hbi_ss', 'script_s', 'script_ss', 'frak_s', 'frak_ss', 'curly_s', 'curly_ss', 'bold_s', 'bold_ss'): if tag not in fonts: continue _, tfm, _ = fonts[tag] sk = SKEW[tag] for (tg, slot), gname in imported.items(): if tg != tag or slot not in tfm.chars or gname in accent_glyphs: continue kern = tfm.kerns.get((slot, sk), 0.0) if sk is not None else 0.0 try: w = tfm.chars[slot]['wd'] ic = tfm.chars[slot].get('ic') or 0.0 out[gname].topaccent = round(((w + ic) / 2 + kern) * 1000) except Exception: pass # Word-compatible primes share local geometry, script ratios, and collision padding. PRIME_SHIFT = None PRIME_PADDING_EVIDENCE = None if 0x2032 in uni_owner: xh_u = round(fonts['syt'][1].fontdimen[5] * 1000) SUP_SHIFT = round(fonts['syt'][1].fontdimen[14] * out.em) try: PRIME_PADDING_EVIDENCE = dict(_SOURCE_SNAPSHOT['geometry']['prime_padding']) PRIME_SHIFT = int(PRIME_PADDING_EVIDENCE['padding']) except Exception as exc: raise RuntimeError('local prime-padding source geometry is missing') from exc _script_scale = float(SCRIPT_RATIO) _targets = [uni_owner[0x2032]] if ('ams', 0x38) in imported: _targets.append(imported[('ams', 0x38)]) for _base in _targets: _s1 = _base + '.ssty1' if _s1 not in out: # script design absent: scale and raise the base glyph. _g = out[_base] _g.transform(psMat.scale(_script_scale)) _bb = _g.boundingBox() _g.transform(psMat.translate(PRIME_SHIFT, xh_u - _bb[1])) _g.width = round(_g.width) continue # base := local package script-size design scaled by the local script ratio out.selection.select(_s1) out.copy() out.selection.select(_base) out.paste() _g = out[_base] _g.transform(psMat.scale(_script_scale)) _bb = _g.boundingBox() _g.transform(psMat.translate(PRIME_SHIFT, xh_u - _bb[1])) _g.width = round(_g.width) _base_y0 = out[_base].boundingBox()[1] # Reposition the first script form. _g1 = out[_s1] _b1 = _g1.boundingBox() _g1.transform(psMat.translate(0, (_base_y0 - SUP_SHIFT) / _script_scale - _b1[1])) # Keep the native scriptscript outline and metrics imported by add_ssty. # Compose repeated primes from the base. pr = uni_owner[0x2032] PW = out[pr].width if pr + '.ssty1' in out: # Use the scaled local script-design advance between primes. st = round(out[pr + '.ssty1'].width * _script_scale) else: st = round(PW - PRIME_SHIFT) compose(0x2033, None, pr, [(0, 0), (st, 0)], PW + st) compose(0x2034, None, pr, [(0, 0), (st, 0), (2 * st, 0)], PW + 2 * st) compose(0x2057, None, pr, [(0, 0), (st, 0), (2 * st, 0), (3 * st, 0)], PW + 3 * st) # Compose repeated script primes from their matching optical forms. for _lvl in (1, 2): _src = f'{pr}.ssty{_lvl}' if _src not in out: continue _stp = out[_src].width for _u, _n in ((0x2033, 2), (0x2034, 3), (0x2057, 4)): _base = uni_owner.get(_u) if not _base: continue _gn = f'{_base}.ssty{_lvl}' if _gn in out: continue out.createChar(-1, _gn) _gl = out[_gn] for _i in range(_n): _gl.addReference(_src, psMat.translate(_i * _stp, 0)) _gl.width = _n * _stp ssty_map.setdefault(_base, []).append(_gn) # Ligate raw repeated primes to avoid repeating collision padding. # Pair adjustment covers shapers that skip the ligature; TeX scripts bypass it. out.addLookup('primelig', 'gsub_ligature', (), (('ccmp', (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable('primelig', 'primelig-1') for _u, _n in ((0x2057, 4), (0x2034, 3), (0x2033, 2)): # Match longer sequences first. _lig = uni_owner.get(_u) if _lig: out[_lig].addPosSub('primelig-1', tuple([pr] * _n)) out.addLookup('primekern', 'gpos_pair', (), (('kern', (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable('primekern', 'primekern-1') out[pr].addPosSub('primekern-1', pr, 0, 0, -PRIME_SHIFT, 0, 0, 0, 0, 0) if ssty_map: out.addLookup('ssty', 'gsub_alternate', (), (('ssty', (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable('ssty', 'ssty-1') for base, alts in ssty_map.items(): try: out[base].addPosSub('ssty-1', tuple(alts)) except Exception: pass # Dotless substitutions for italic i/j and upright i. dtls_pairs = [] if 0x1D456 in uni_owner and 0x1D6A4 in uni_owner: dtls_pairs.append((uni_owner[0x1D456], uni_owner[0x1D6A4])) if 0x1D457 in uni_owner and 0x1D6A5 in uni_owner: dtls_pairs.append((uni_owner[0x1D457], uni_owner[0x1D6A5])) if 0x0069 in uni_owner and upright_dotless: dtls_pairs.append((uni_owner[0x0069], upright_dotless)) if bold_it_latin.get('i') and bold_dotless_i: dtls_pairs.append((bold_it_latin['i'], bold_dotless_i)) if bold_it_latin.get('j') and bold_dotless_j: dtls_pairs.append((bold_it_latin['j'], bold_dotless_j)) if dtls_pairs: out.addLookup('dtls', 'gsub_single', (), (('dtls', (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable('dtls', 'dtls-1') for base, sub in dtls_pairs: out[base].addPosSub('dtls-1', sub) # Calligraphic salt maps curly to script. curly_pairs = [] for slot, (u, name) in U.alpha_map('curly').items(): key = ('curly', slot) if key in imported and ('script', slot) in imported: curly_pairs.append((imported[('script', slot)], imported[key])) # Keep varnothing in cv02 so calligraphic salt cannot change it. if curly_pairs: out.addLookup('salt', 'gsub_alternate', (), (('salt', (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable('salt', 'salt-1') for base, alt in curly_pairs: try: out[base].addPosSub('salt-1', (alt,)) except Exception: pass # Expose alternate codepoints for the same glyph. for base_u, extra_u in [(0x007C, 0x2223), (0x2016, 0x2225), (0x1D715, 0x2202)]: if base_u in uni_owner and extra_u not in uni_owner: g = out[uni_owner[base_u]] g.altuni = ((g.altuni or ()) + ((extra_u, -1, 0),)) uni_owner[extra_u] = g.glyphname # Build cumulative spacing after all alphabet imports: hmtx keeps CHARWD, # MATH keeps CHARIC, and GPOS adds ordinary TFM kern plus CHARIC(left). # Exclude family skewchar-right pairs from ordinary kerning. # cv01: swash z. zalt_pairs = [] if ('mit', 180) in imported and 0x1D467 in uni_owner: zalt_pairs.append((uni_owner[0x1D467], imported[('mit', 180)])) for lvl, t in ((1, 'mit_s'), (2, 'mit_ss')): if t in fonts and 180 in fonts[t][1].chars: alt = import_glyph(t, 180, name=f'z.alt.ssty{lvl}') basek = (t, 0x7A) if alt and basek in imported: zalt_pairs.append((imported[basek], alt)) # cv03..cv07: blackboard and holey alphabet options. for cvn, tag in (('cv03', 'bbd'), ('cv04', 'bbi'), ('cv05', 'hrb'), ('cv06', 'hrbd'), ('cv07', 'hbi')): if tag not in fonts: continue pairs = [] for slot, (u, nm) in U.alpha_map('bb').items(): alt = import_glyph(tag, slot, name=f'{nm}.{tag}') if alt and u in uni_owner: base = uni_owner[u] pairs.append((base, alt)) # Add script-form pairs for either shaping order: ssty then cv, or cv then ssty. for lvl in (1, 2): bb, aa = f'{base}.ssty{lvl}', f'{alt}.ssty{lvl}' if bb in out and aa in out: pairs.append((bb, aa)) if not pairs: continue out.addLookup(cvn, 'gsub_single', (), ((cvn, (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable(cvn, cvn + '-1') for base, alt in pairs: try: out[base].addPosSub(cvn + '-1', alt) except Exception: pass # cv02: slashed-zero and circular empty-set forms. if ('ams', 191) in imported and 0x2205 in uni_owner: out.addLookup('cv02', 'gsub_single', (), (('cv02', (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable('cv02', 'cv02-1') try: out[uni_owner[0x2205]].addPosSub('cv02-1', imported[('ams', 191)]) except Exception: pass if zalt_pairs: out.addLookup('cv01', 'gsub_single', (), (('cv01', (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable('cv01', 'cv01-1') for base, alt in zalt_pairs: try: out[base].addPosSub('cv01-1', alt) except Exception: pass def with_ssty(pairs): """Extend substitution pairs with matching optical script forms.""" res = [] for b, a in pairs: if not b or not a: continue res.append((b, a)) for lvl in (1, 2): bb, aa = f'{b}.ssty{lvl}', f'{a}.ssty{lvl}' if bb in out and aa in out: res.append((bb, aa)) return res def add_cv(tag, pairs): """Register a single-substitution character-variant feature.""" pairs = [(b, a) for b, a in with_ssty(pairs) if b in out and a in out] if not pairs: return out.addLookup(tag, 'gsub_single', (), ((tag, (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable(tag, tag + '-1') for base, alt in pairs: try: out[base].addPosSub(tag + '-1', alt) except Exception: pass # cv08: straight braces. add_cv('cv08', [(uni_owner.get(0x007B), 'braceleft.straight'), (uni_owner.get(0x007D), 'braceright.straight')]) # cv09: AMS negated relations; defaults retain MTPro2 forms. add_cv('cv09', [(uni_owner.get(u), imported.get(('ams', slot))) for u, slot in ((0x2241, 156), (0x2288, 170), (0x2289, 171))]) # cv10: variant subsetneq and supsetneq. add_cv('cv10', [(uni_owner.get(u), imported.get(('ams', slot))) for u, slot in ((0x228A, 160), (0x228B, 161))]) # cv11: hbar and hslash. add_cv('cv11', [(uni_owner.get(0x210F), imported.get(('syt', 175)))]) # cv12: slanted large operators. add_cv('cv12', [(uni_owner.get(u), imported.get(('exa', slot))) for u, slot in ((0x2211, 160), (0x220F, 162), (0x2210, 164))]) # Provide Word/OMML spaces and invisible operators to prevent fallback. BLANKS = {0x0020: 333, 0x00A0: 333, 0x2000: 500, 0x2001: 1000, 0x2002: 500, 0x2003: 1000, 0x2004: 333, 0x2005: 250, 0x2006: 167, 0x2007: 500, 0x2008: 250, 0x2009: 200, 0x200A: 100, 0x200B: 0, 0x202F: 200, 0x205F: 222, 0x2061: 0, 0x2062: 0, 0x2063: 0, 0x2064: 0} for u, w in BLANKS.items(): if u in uni_owner: continue gn = 'space' if u == 0x0020 else uname(u) out.createChar(u, gn) out[gn].width = w uni_owner[u] = gn # Reserve math and non-Roman ranges against text-donor substitution. TEXT_ONLY_SKIP = ( (0x0370, 0x03FF), # Greek (0x0400, 0x052F), # Cyrillic (0x2100, 0x214F), # Letterlike symbols (0x2190, 0x21FF), # Arrows (0x2200, 0x22FF), # Math operators (0x2300, 0x23FF), # Technical symbols (0x25A0, 0x25FF), # Geometric shapes (0x27C0, 0x27EF), # Supplemental math A (0x2980, 0x2AFF), # Supplemental math B (0x1D400, 0x1D7FF), # Math alphanumerics ) # Allow text-only letterlike symbols, but reserve math-active primes. TEXT_ONLY_PRIMES = set(range(0x2032, 0x2038)) | {0x2057} TEXT_ONLY_ALLOW = { 0x2105, 0x2116, 0x211E, 0x2120, 0x2121, 0x2122, 0x212E, } def _is_math_range(u): if u in TEXT_ONLY_PRIMES: return True if u in TEXT_ONLY_ALLOW: return False return any(a <= u <= b for a, b in TEXT_ONLY_SKIP) # Limit donor additions to Latin/Common text; math ownership takes precedence. ROMAN_TEXT_EXTRA_RANGES = ( (0x0020, 0x036F), (0x1E00, 0x1EFF), (0x2000, 0x218F), (0x2500, 0x266F), (0x301A, 0x301B), (0xFB00, 0xFB06), ) def _is_roman_text_extra(u): return any(first <= u <= last for first, last in ROMAN_TEXT_EXTRA_RANGES) # Reserve Full-only source codepoints in Lite, including AMSa eth. CANONICAL_FULL_ONLY_SOURCE_UNICODE = set() for _u, _name in U.AMSA.values(): if _u is not None: CANONICAL_FULL_ONLY_SOURCE_UNICODE.add(_u) for _kind in ('bb', 'script', 'frak', 'curly'): for _slot, (_u, _name) in U.alpha_map(_kind).items(): if _u is not None: CANONICAL_FULL_ONLY_SOURCE_UNICODE.add(_u) # Text-donor symbols used directly by Word math. ROMAN_EXTRA = {'percent': 0x0025, 'ampersand': 0x0026, 'at': 0x0040, 'underscore': 0x005F, 'asciitilde': 0x007E, 'numbersign': 0x0023, 'dollar': 0x0024, 'degree': 0x00B0, 'fraction': 0x2044, 'question': 0x003F, 'exclam': 0x0021} if os.path.exists(ROMAN_REG): nf2 = fontforge.open(ROMAN_REG) _roman_extra_scale = float(out.em) / float(nf2.em) for src_name, u in ROMAN_EXTRA.items(): if u in uni_owner or src_name not in nf2: continue if EDITION == 'lite' and u in CANONICAL_FULL_ONLY_SOURCE_UNICODE: continue nf2.selection.select(src_name) nf2.copy() gn = uname(u) out.createChar(u, gn) out.selection.select(gn) out.paste() if abs(_roman_extra_scale - 1.0) > 1e-12: out[gn].transform(psMat.scale(_roman_extra_scale)) out[gn].width = round(nf2[src_name].width * _roman_extra_scale) uni_owner[u] = gn # Fill remaining text coverage without replacing existing math glyphs. todo = [] for g in nf2.glyphs(): u = g.unicode if u is None or u < 0x20 or u in uni_owner: continue if not _is_roman_text_extra(u): continue # selectable donor must not add unrelated scripts if EDITION == 'lite' and u in CANONICAL_FULL_ONLY_SOURCE_UNICODE: continue # Full-only MTPro2 semantic remains a hole in Lite if _is_math_range(u): continue # Reserve math glyphs. if 0xE000 <= u <= 0xF8FF or u in (0xFEFF, 0xFFFD): continue # Exclude private and special-use ranges. gn = uname(u) if gn in out: continue todo.append((g.glyphname, u, gn, g.width)) for src_name, u, gn, w in todo: nf2.selection.select(src_name) nf2.unlinkReferences() # Flatten references before copying outlines. nf2.selection.select(src_name) nf2.copy() out.createChar(u, gn) out.selection.select(gn) out.paste() if abs(_roman_extra_scale - 1.0) > 1e-12: out[gn].transform(psMat.scale(_roman_extra_scale)) out[gn].width = round(w * _roman_extra_scale) uni_owner[u] = gn nf2.close() # Compose proportion from two colons. if 0x2237 not in uni_owner and 0x003A in uni_owner: c = uni_owner[0x003A] cw = out[c].width compose(0x2237, None, c, [(0, 0), (cw + 167, 0)], 2 * cw + 167) # Expose private alternatives for Word, where cv/salt controls are unavailable. PUA = 0xE000 def pua(gname): global PUA if gname not in out: return g = out[gname] if g.unicode == -1: g.unicode = PUA else: g.altuni = ((g.altuni or ()) + ((PUA, -1, 0),)) PUA += 1 if ('mit', 180) in imported: pua(imported[('mit', 180)]) # U+E000 z swash if ('ams', 191) in imported: pua(imported[('ams', 191)]) # U+E001 varnothing PUA = 0xE010 for slot, (u, nm) in sorted(U.alpha_map('curly').items()): if ('curly', slot) in imported: pua(imported[('curly', slot)]) # U+E010.. curly for base, tag in ((0xE100, 'bbd'), (0xE140, 'bbi'), (0xE180, 'hrb'), (0xE1C0, 'hrbd'), (0xE200, 'hbi')): PUA = base for slot, (u, nm) in sorted(U.alpha_map('bb').items()): if (tag, slot) in imported: pua(imported[(tag, slot)]) # Keep private assignments fixed across editions; missing glyphs leave holes. PUA_FIXED = { 0xE23E: 'lhook', 0xE23F: 'rhook', 0xE240: 'varbeta.it', 0xE241: 'vardelta.it', 0xE242: 'upvardelta', 0xE243: 'dbar.it', 0xE244: 'updbar', 0xE245: 'negationslash', 0xE246: 'mapstochar', 0xE247: 'tie.sy', 0xE248: 'compose', 0xE249: 'Relbar', 0xE24A: 'wwbar', 0xE24B: 'dotup.accent', 0xE24C: 'ddotup.accent', 0xE24D: 'smallint', 0xE24E: 'wbar', 0xE24F: 'what', 0xE250: 'wtilde', 0xE251: 'wcheck', 0xE252: 'clubshaded', 0xE253: 'spadeshaded', 0xE254: 'dddotup.accent', 0xE255: 'ddddotup.accent', 0xE256: 'hslash', 0xE257: 'simarrow', 0xE258: 'varland', 0xE259: 'contraction', 0xE25A: 'circdashbullet', 0xE25B: 'bulletdashcirc', 0xE25C: 'braceleft.straight', 0xE25D: 'braceright.straight', 0xE25E: 'midshaft', 0xE25F: 'rarrowhead', 0xE260: 'larrowhead', 0xE261: 'varpropto', 0xE262: 'smallsmile', 0xE263: 'smallfrown', 0xE264: 'lvertneqq', 0xE265: 'gvertneqq', 0xE266: 'nleqslant', 0xE267: 'ngeqslant', 0xE268: 'npreceq', 0xE269: 'nsucceq', 0xE26A: 'nleqq', 0xE26B: 'ngeqq', 0xE26C: 'nsim.ams', 0xE26D: 'varsubsetneq', 0xE26E: 'varsupsetneq', 0xE26F: 'nsubseteqq', 0xE270: 'nsupseteqq', 0xE271: 'varsubsetneqq', 0xE272: 'varsupsetneqq', 0xE273: 'nsubseteq.ams', 0xE274: 'nsupseteq.ams', 0xE275: 'nshortmid', 0xE276: 'nshortparallel', 0xE277: 'shortmid', 0xE278: 'shortparallel', 0xE279: 'thicksim', 0xE27A: 'thickapprox', 0xE27B: 'nsqsubset', 0xE27C: 'nsqsupset', 0xE27D: 'leadsto', 0xE27E: 'undercurvearrowleft', 0xE27F: 'undercurvearrowright', 0xE280: 'capprod.big', 0xE281: 'slsum', 0xE282: 'slprod', 0xE283: 'slcoprod', 0xE284: 'varland.big', 0xE285: 'ast.big', 0xE286: 'wideoverbar.compat', 0xE287: 'widevector.compat', } _pua_done = set() _maxcp = 0xE23D for _cp in sorted(PUA_FIXED): _maxcp = max(_maxcp, _cp) _nm = PUA_FIXED[_cp] if _nm in out and _nm not in _pua_done and out[_nm].unicode == _cp: _pua_done.add(_nm) elif _nm in out and _nm not in _pua_done and out[_nm].unicode == -1: out[_nm].unicode = _cp _pua_done.add(_nm) elif _nm in out and _nm not in _pua_done: out[_nm].altuni = ((out[_nm].altuni or ()) + ((_cp, -1, 0),)) _pua_done.add(_nm) PUA = _maxcp + 1 # Unencoded mapped glyphs are assigned deterministically after the fixed range, # in source-table slot order. for _tag, _table in (('mit', U.MIT), ('syt', U.SYT), ('ams', U.AMSA)): for _slot, _ent in sorted(_table.items()): _u, _nm = _ent[0], _ent[1] if _u is not None or not _nm: continue _g = imported.get((_tag, _slot)) if _g and _g not in _pua_done and _g in out and out[_g].unicode == -1: pua(_g) _pua_done.add(_g) for _slot in sorted(U.EXA_OPS): _u, _nm = U.EXA_OPS[_slot][0], U.EXA_OPS[_slot][1] if _u is not None or not _nm: continue _g = imported.get(('exa', _slot)) if _g and _g not in _pua_done and _g in out and out[_g].unicode == -1: pua(_g) _pua_done.add(_g) for _nm in ('braceleft.straight', 'braceright.straight'): if _nm in out and out[_nm].unicode == -1 and _nm not in _pua_done: pua(_nm) _pua_done.add(_nm) # PUA identities are part of the font contract; no TeX sidecar is generated. # Metrics and output. out.ascent, out.descent = 800, 200 # Use absolute FontForge metrics rather than offsets from the bounding box. out.os2_typoascent_add = out.os2_typodescent_add = 0 out.hhea_ascent_add = out.hhea_descent_add = 0 out.os2_typoascent, out.os2_typodescent, out.os2_typolinegap = 806, -194, 200 out.hhea_ascent, out.hhea_descent, out.hhea_linegap = 806, -194, 200 out.os2_xheight = round(sy_fd[5] * 1000) # Read cap height from the local math-italic TFM. cap_h = (round(fonts['mit'][1].chars[0x41]['ht'] * out.em) if 0x41 in fonts['mit'][1].chars else None) if cap_h: out.os2_capheight = cap_h # Cover all outlines with Windows clipping bounds; USE_TYPO_METRICS controls spacing. # Scan glyph bounds for compatibility across FontForge versions. _ymin, _ymax = 0, 0 for _gl in out.glyphs(): _bb = _gl.boundingBox() if _bb[1] == _bb[3] == 0 and _bb[0] == _bb[2] == 0: continue _ymin = min(_ymin, _bb[1]) _ymax = max(_ymax, _bb[3]) out.os2_winascent = max(806, int(math.ceil(_ymax))) out.os2_winascent_add = 0 out.os2_windescent = max(194, int(math.ceil(-_ymin))) out.os2_windescent_add = 0 out.os2_use_typo_metrics = 1 out.os2_vendor = 'MTP2' # Preserve the source trademark notice. try: out.appendSFNTName('English (US)', 'Trademark', 'MathTime is a trademark of Publish or Perish, Inc.') except Exception: pass try: out.appendSFNTName('English (US)', 'Preferred Family', FAMILY) out.appendSFNTName('English (US)', 'Preferred Styles', STYLE) except Exception: pass out.os2_stylemap = 0x40 # Apply the selected embedding flag; license terms remain separate. out.os2_fstype = FSTYPE # finalize.py sets the Windows-compatible technical font version. # Word math-italic nabla aliases the source nabla glyph. if 0x2207 in uni_owner: _ng = out[uni_owner[0x2207]] _nabla_aliases = [0x1D6FB] for _u in _nabla_aliases: if _u not in uni_owner: _ng.altuni = (_ng.altuni or ()) + ((_u, -1, 0),) uni_owner[_u] = _ng.glyphname # Alias the fixed bar to U+0305 for Word. if 0x0304 in uni_owner and 0x0305 not in uni_owner: _g = out[uni_owner[0x0304]] _g.altuni = (_g.altuni or ()) + ((0x0305, -1, 0),) uni_owner[0x0305] = uni_owner[0x0304] # Finalize TopAccent and ordinary spacing after all source and alternate imports. # Source roles, not nonzero IC alone, determine ordinary-character membership. def _source_is_ordinary_left(tag, slot): return source_policy.is_ordinary_left(tag, slot) if 'wideoverbar.compat' in out: accent_glyphs.add('wideoverbar.compat') if 'widevector.compat' in out: accent_glyphs.add('widevector.compat') # Finite source accents retain positive advance and attach at its center. _source_fixed_accent_names = frozenset(( # Normalized dot accents instead retain zero advance and ink-center attachment. 'wbar', 'wwbar', 'what', 'wtilde', 'wcheck', )) _source_fixed_accent_glyphs = set() for _agn in _source_fixed_accent_names: if _agn in out: accent_glyphs.add(_agn) _source_fixed_accent_glyphs.add(_agn) # Apply family skewchar policy after every source alphabet has been imported. for (_tag, _slot), _gname in sorted(imported.items()): if _tag not in SOURCE_SKEW or _tag not in fonts or _gname not in out: continue _tfm = fonts[_tag][1] if _slot not in _tfm.chars: continue if _gname in _source_fixed_accent_glyphs: _ta = out[_gname].width // 2 elif _gname in accent_glyphs: _ta = ACC_CENTER else: _wd = _tfm.chars[_slot].get('wd') or 0.0 _ic = _tfm.chars[_slot].get('ic') or 0.0 _sk = SOURCE_SKEW[_tag] _skern = (_tfm.kerns.get((_slot, _sk), 0.0) or 0.0) if _sk is not None else 0.0 _ta = round(((_wd + _ic) / 2 + _skern) * 1000) try: out[_gname].topaccent = int(_ta) except Exception: pass # U+E286 uses positive-width bar coordinates; U+E287 uses zero-advance vector coordinates. if 'wideoverbar.compat' in out: out['wideoverbar.compat'].topaccent = out['wideoverbar.compat'].width // 2 if 'widevector.compat' in out: out['widevector.compat'].topaccent = ACC_CENTER # Serialize TFM kern alone; add_ordinary_ic_gpos.py adds the independent IC layer. out.addLookup('kernL', 'gpos_pair', (), (('kern', (('DFLT', ('dflt',)), ('latn', ('dflt',)), ('math', ('dflt',)))),)) out.addLookupSubtable('kernL', 'kern-1') _source_tfm_pair_contract = {} def _remember_source_tfm_pair(gl, gr, rec): key = (gl, gr) prev = _source_tfm_pair_contract.get(key) if prev is not None: if int(prev['xadvance']) != int(rec['xadvance']): raise RuntimeError('conflicting source TFM kern %s/%s: %+d vs %+d' % (gl, gr, prev['xadvance'], rec['xadvance'])) return _source_tfm_pair_contract[key] = rec out[gl].addPosSub('kern-1', gr, 0, 0, int(rec['xadvance']), 0, 0, 0, 0, 0) for _tag in tuple(SOURCE_SKEW): if _tag not in fonts: continue _tfm = fonts[_tag][1] _sk = SOURCE_SKEW[_tag] for (_l, _r), _v in sorted(_tfm.kerns.items()): if _sk is not None and _r == _sk: continue _gl = imported.get((_tag, _l)) _gr = imported.get((_tag, _r)) if not _gl or not _gr or _gl not in out or _gr not in out: continue _k = round((_v or 0.0) * 1000) if not _k: continue _remember_source_tfm_pair(_gl, _gr, { 'left': _gl, 'right': _gr, 'xadvance': int(_k), 'tag': _tag, 'left_slot': int(_l), 'right_slot': int(_r), 'skewchar': _sk, 'formula': 'ordinary TFMkern(left,right)', }) # Builder source map used by independent post-serialization/audit tools. _source_metric_contract = [] _ordinary_ic_left_by_glyph = {} for (_tag, _slot), _gname in sorted(imported.items()): if _tag not in SOURCE_SKEW or _tag not in fonts or _gname not in out: continue _pfb, _tfm, _slots = fonts[_tag] if _slot not in _tfm.chars or _slot not in _slots: continue _srcname = _slots[_slot] _wd = round((_tfm.chars[_slot].get('wd') or 0.0) * 1000) _ic = round((_tfm.chars[_slot].get('ic') or 0.0) * 1000) _sk = SOURCE_SKEW[_tag] _skern = round(((_tfm.kerns.get((_slot, _sk), 0.0) or 0.0) if _sk is not None else 0.0) * 1000) # Recompute the exact target for the JSON record. if _gname in _source_fixed_accent_glyphs: _target_ta = int(out[_gname].width // 2) elif _gname in accent_glyphs: _target_ta = int(ACC_CENTER) else: _wd_f = _tfm.chars[_slot].get('wd') or 0.0 _ic_f = _tfm.chars[_slot].get('ic') or 0.0 _target_ta = round(((_wd_f + _ic_f) / 2 + ((_tfm.kerns.get((_slot, _sk), 0.0) or 0.0) if _sk is not None else 0.0)) * 1000) try: _sbb = tuple(round(v) for v in _pfb[_srcname].boundingBox()) except Exception: _sbb = None _rec = { 'tag': _tag, 'slot': int(_slot), 'source_glyph': _srcname, 'output_glyph': _gname, 'width': int(_wd), 'italic_correction': int(_ic), 'skewchar': _sk, 'skew_kern': int(_skern), 'topaccent_target': int(_target_ta), 'ordinary_left': bool(_source_is_ordinary_left(_tag, _slot)), 'normalized_combining_accent': bool(_gname in accent_glyphs and _gname not in _source_fixed_accent_glyphs), 'fixed_source_accent': bool(_gname in _source_fixed_accent_glyphs), 'source_bbox': _sbb, } _source_metric_contract.append(_rec) if _ic and _rec['ordinary_left']: _prev = _ordinary_ic_left_by_glyph.get(_gname) if _prev is not None and int(_prev['italic_correction']) != int(_ic): raise RuntimeError('conflicting ordinary IC for %s: %d vs %d' % (_gname, _prev['italic_correction'], _ic)) _ordinary_ic_left_by_glyph[_gname] = { 'glyph': _gname, 'italic_correction': int(_ic), 'tag': _tag, 'slot': int(_slot), } # GDEF marks combining accents as marks and other glyphs as bases. for _g in out.glyphs(): _u = _g.unicode if _u == -1 and _g.altuni: _u = _g.altuni[0][0] if (_g.glyphname in accent_glyphs or (0x0300 <= _u <= 0x036F) or (0x20D0 <= _u <= 0x20F0)): _g.glyphclass = 'mark' else: _g.glyphclass = 'baseglyph' # Write the local source contract for independent post-serialization audits. _source_contract_path = f'{OUTDIR}/mtpro2-source-contract.json' json.dump({ 'policy': ( 'MTPro2 original LaTeX source contract: hmtx=CHARWD; MATH IC=CHARIC; ' 'ordinary OpenType adjacency = CHARIC(left) + ordinary TFM kern(left,right), ' 'implemented as two cumulative GPOS lookups; missing TFM kern=0; ' 'per-family skewchar-right records excluded according to mtpro2.sty/umt2*.fd; ' 'scripts/primes/accents remain separate semantic paths; Word may segment ' 'math-class boundaries before GPOS.' ), 'skewchar_by_tag': {k: SOURCE_SKEW[k] for k in SOURCE_SKEW if k in fonts}, 'source_font_by_tag': {k: SRC[k] for k in SOURCE_SKEW if k in fonts}, 'prime_padding': globals().get('PRIME_PADDING_EVIDENCE'), 'records': _source_metric_contract, 'ordinary_ic_lefts': [ dict(v) for (_g, v) in sorted(_ordinary_ic_left_by_glyph.items()) ], 'tfm_kern_pairs': [ dict(v) for (_key, v) in sorted(_source_tfm_pair_contract.items()) ], }, open(_source_contract_path, 'w', encoding='utf-8'), ensure_ascii=False, indent=2) # The OpenType font is the unicode-math interface; no callback is generated. # Serialize MinConnectorOverlap from the local connector geometry. MINOVL = int(getattr(out.math, 'MinConnectorOverlap', MINOVL) or 0) ALL_MATH_SCALARS['MinConnectorOverlap'] = MINOVL _math_values_path = f'{OUTDIR}/math-constants.json' json.dump({'upm': out.em, 'constants': dict(C), 'MinConnectorOverlap': MINOVL, 'provenance': math_constants.audit_payload(ALL_MATH_SCALARS)}, open(_math_values_path, 'w', encoding='utf-8'), ensure_ascii=False, indent=2) outpath = f'{OUTDIR}/{FONT_BASENAME}.otf' out.generate(outpath, flags=('opentype',)) # Normalize structures that are safer to edit after FontForge generation. _tools = os.path.dirname(os.path.abspath(__file__)) _system_python = os.environ['MTP2_SYSTEM_PYTHON'] def _run_tool(script, *args): cmd = [_system_python, os.path.join(_tools, script)] + [str(x) for x in args] subprocess.run(cmd, check=True) _run_tool('finalize.py', outpath, '--edition', EDITION, '--math-values', _math_values_path) # Add the IC class lookup after serialization; it accumulates with TFM kern. _run_tool('add_ordinary_ic_gpos.py', outpath, '--contract', _source_contract_path) if PRIME_SHIFT is not None: _run_tool('primectx.py', outpath, '--base-shift', PRIME_SHIFT)