"""Source-profile fixed designs: provenance order and isolated cv13-19 clones. Only source identities and structural rules live here. Outlines and numeric metrics are read from the current build, never from a diagnostic font. """ import copy from fontTools.ttLib.tables import otTables as ot from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.t2CharStringPen import T2CharStringPen from fontTools.pens.transformPen import TransformPen from finalize import update_average_width SPECS = ( ('cv13', 9182, (('exe', 176, 192), ('exf', 160, 171), ('exg', 160, 165))), ('cv14', 9183, (('exe', 144, 160), ('exf', 144, 155), ('exg', 144, 149))), ( 'cv15', 9180, ( ('exa', 190, 194), ('exe', 208, 214), ('exf', 177, 183), ('exg', 177, 183), ('exa', 195, 195) ) ) ) EXTRA = ( ('cv16', 40, (('exg', 0),)), ('cv16', 41, (('exg', 1),)), ('cv16', 123, (('exf', 8), ('exg', 8))), ('cv16', 125, (('exf', 9), ('exg', 9))), ('cv16', 10216, (('exg', 10),)), ('cv16', 10217, (('exg', 11),)), ('cv16', 92, (('exg', 15),)), ('cv17', 8730, (('exf', 112), ('exg', 112))), ('cv18', 770, (('exe', 57), ('exf', 59))), ('cv18', 771, (('exa', 103),)), ('cv18', 780, (('exe', 89), ('exf', 91))), ('cv19', 123, (('exa', 182), ('exa', 184), ('exa', 186), ('exa', 188))), ('cv19', 125, (('exa', 183), ('exa', 185), ('exa', 187), ('exa', 189))) ) TAGS = tuple(dict.fromkeys([x[0] for x in SPECS + EXTRA] + ['cv20', 'cv21'])) SCRIPT_FIXED_INPUTS = { 'cv16': (40, 41, 123, 125, 10216, 10217, 92), 'cv17': (8730,), 'cv19': (123, 125) } def add_script_fixed_inputs(font, tag, alternates): """Add S/SS inputs atomically, using actual ssty rather than glyph names. This component is also usable on a copy with existing T fixed selectors; it neither installs clones nor changes any font table by itself. """ if tag not in SCRIPT_FIXED_INPUTS: raise ValueError('unsupported script fixed feature ' + tag) if 'GSUB' not in font: raise ValueError('missing GSUB for script fixed inputs') g = font['GSUB'].table features = [ r for r in g.FeatureList.FeatureRecord if r.FeatureTag == 'ssty' ] if len(features) != 1: raise ValueError('ssty must have one feature record') feature = features[0].Feature indices = feature.LookupListIndex if feature.LookupCount != 1 or len(indices) != 1: raise ValueError('ssty must reference one lookup') index = indices[0] if not 0 <= index < len(g.LookupList.Lookup): raise ValueError('ssty lookup index out of range') lookup = g.LookupList.Lookup[index] if ( lookup.LookupType != 3 or lookup.LookupFlag != 0 or lookup.SubTableCount != 1 or (len(lookup.SubTable) != 1) or (not hasattr(lookup.SubTable[0], 'alternates')) ): raise ValueError('ssty must be one unflagged AlternateSubst subtable') cmap = font.getBestCmap() glyphs = set(font.getGlyphOrder()) pending = {} rows = [] for cp in SCRIPT_FIXED_INPUTS[tag]: owner = cmap.get(cp) forms = alternates.get(owner) if ( owner not in glyphs or not forms or any((n not in glyphs for n in forms)) ): raise ValueError( f'{tag} U+{cp:04X}: missing T owner or fixed candidates' ) styled = lookup.SubTable[0].alternates.get(owner) if ( not styled or len(styled) != 2 or len(set([owner] + list(styled))) != 3 or any((n not in glyphs for n in styled)) ): raise ValueError( f'{tag} U+{cp:04X}: ssty must select distinct S and SS glyphs' ) for level, key in enumerate(styled, 1): if key in alternates or key in pending: raise ValueError( f'{tag} U+{cp:04X}: existing or colliding S/SS input ' f'{key}' ) pending[key] = list(forms) rows.append( { 'feature': tag, 'unicode': cp, 'style': level, 'ssty_lookup_index': index, 'T_owner_gid': font.getGlyphID(owner), 'styled_owner_gid': font.getGlyphID(key), 'target_gids': [font.getGlyphID(n) for n in forms] } ) if len(pending) != 2 * len(SCRIPT_FIXED_INPUTS[tag]): raise ValueError(tag + ': script fixed input count differs') alternates.update(pending) return rows def records(): old = [ (tag, cp, family, slot) for tag, cp, families in SPECS for family, start, end in families for slot in range(start, end + 1) ] return old + [ (tag, cp, family, slot) for tag, cp, forms in EXTRA for family, slot in forms ] def copy_math_attributes(font, source, clone): gi = font['MATH'].table.MathGlyphInfo state = {} for attr, covfield, valuesfield, countfield in ( ( 'MathItalicsCorrectionInfo', 'Coverage', 'ItalicsCorrection', 'ItalicsCorrectionCount' ), ( 'MathTopAccentAttachment', 'TopAccentCoverage', 'TopAccentAttachment', 'TopAccentAttachmentCount' ), ( 'MathKernInfo', 'MathKernCoverage', 'MathKernInfoRecords', 'MathKernCount' ) ): table = getattr(gi, attr, None) cov = getattr(table, covfield, None) if table else None present = bool(cov and source in cov.glyphs) state[attr] = present if present: old = getattr(table, valuesfield)[cov.glyphs.index(source)] cov.glyphs.append(clone) getattr(table, valuesfield).append(copy.deepcopy(old)) setattr(table, countfield, len(cov.glyphs)) ext = gi.ExtendedShapeCoverage present = bool(ext and source in ext.glyphs) state['ExtendedShapeCoverage'] = present if present: ext.glyphs.append(clone) gdef = font['GDEF'].table for attr in ('GlyphClassDef', 'MarkAttachClassDef'): table = getattr(gdef, attr, None) classes = table.classDefs if table else {} state['GDEF.' + attr] = classes.get(source) if source in classes: classes[clone] = classes[source] for attr, covfield, valfield, countfield in ( ('AttachList', 'Coverage', 'AttachPoint', 'GlyphCount'), ('LigCaretList', 'Coverage', 'LigGlyph', 'LigGlyphCount') ): table = getattr(gdef, attr, None) cov = getattr(table, covfield, None) if table else None present = bool(cov and source in cov.glyphs) state['GDEF.' + attr] = present if present: value = getattr(table, valfield)[cov.glyphs.index(source)] cov.glyphs.append(clone) getattr(table, valfield).append(copy.deepcopy(value)) setattr(table, countfield, len(cov.glyphs)) return state def bounds(font, name): gs = font.getGlyphSet() pen = BoundsPen(gs) gs[name].draw(pen) if pen.bounds is None: raise ValueError('empty fixed source outline ' + name) return pen.bounds def translated_charstring(font, name, dy): """Draw CFF source at (0,dy), retaining its original horizontal advance. """ cs = font['CFF '].cff.topDictIndex[0].CharStrings[name] gs = font.getGlyphSet() pen = T2CharStringPen(font['hmtx'][name][0] - cs.private.nominalWidthX, gs) gs[name].draw(TransformPen(pen, (1, 0, 0, 1, 0, dy))) return pen.getCharString( private=cs.private, globalSubrs=cs.globalSubrs, optimize=True ) def install(font, inventory): if 'CFF ' not in font: raise ValueError('install fixed selectors before TrueType conversion') imports = {(r['tag'], r['slot']): r['glyph'] for r in inventory['imports']} original = list(font.getGlyphOrder()) m = font['MATH'].table.MathVariants cons = dict(zip(m.HorizGlyphCoverage.glyphs, m.HorizGlyphConstruction)) gsub = font['GSUB'].table old_features = list(gsub.FeatureList.FeatureRecord) if any( (r.FeatureTag in TAGS + ('cv97', 'cv98', 'cv99') for r in old_features) ): raise ValueError('fixed selector tags already present') root_changes = [] for cp, tag, slot in ((9182, 'exe', 176), (9183, 'exe', 144)): source = imports.get((tag, slot)) ordinary = f'uni{cp:04X}' if ( not source or source != font.getBestCmap().get(cp) or ordinary not in cons ): raise ValueError( f'brace root/source mismatch U+{cp:04X} {tag}:{slot}' ) if ( cons[source].VariantCount != 35 or cons[ordinary].VariantCount != 35 ): raise ValueError('source fixed brace series changed') if ( not cons[ordinary].GlyphAssembly or len(cons[ordinary].GlyphAssembly.PartRecords) != 5 ): raise ValueError('legacy ordinary brace assembly missing') cons[ordinary].MathGlyphVariantRecord = [] cons[ordinary].VariantCount = 0 for table in font['cmap'].tables: if table.isUnicode() and cp in table.cmap: table.cmap[cp] = ordinary root_changes.append( { 'unicode': f'U+{cp:04X}', 'ordinary_root': ordinary, 'retained_source_root': source, 'ordinary_variant_count': 0, 'assembly_parts': 5 } ) top = font['CFF '].cff.topDictIndex[0] cs = top.CharStrings mapping = [] targets = {tag: {} for tag in TAGS} translations = [] terminals = [] axis = font['MATH'].table.MathConstants.AxisHeight.Value for tag, cp, family, slot in records(): source = imports.get((family, slot)) sid = f'{family}:{slot}:{tag}' if source not in original: raise ValueError('missing fixed source ' + sid) clone = source + '.fixed' if clone in font.getGlyphOrder() or clone in cs.charStrings: raise ValueError('duplicate fixed source ' + sid) raw_bbox = bounds(font, source) if tag in ('cv17', 'cv19') else None if tag == 'cv19': if ( font['MATH'].table.MathGlyphInfo.MathKernInfo and source in ( font['MATH'].table.MathGlyphInfo.MathKernInfo .MathKernCoverage.glyphs if font['MATH'].table.MathGlyphInfo.MathKernInfo .MathKernCoverage else [] ) ): raise ValueError( 'cv19 has vertical-position-dependent MathKernInfo: ' + sid ) dy = axis - (raw_bbox[1] + raw_bbox[3]) / 2 outline = translated_charstring(font, source, dy) translations.append( { 'source_id': sid, 'source_glyph': source, 'clone_glyph': clone, 'axis_height': axis, 'translation_y': dy, 'source_bbox': list(raw_bbox), 'clone_bbox': [ raw_bbox[0], raw_bbox[1] + dy, raw_bbox[2], raw_bbox[3] + dy ], 'hmtx': list(font['hmtx'][source]) } ) else: outline = copy.deepcopy(cs[source]) cs.charStrings[clone] = len(cs.charStringsIndex) cs.charStringsIndex.items.append(outline) top.charset.append(clone) font['hmtx'].metrics[clone] = font['hmtx'][source] attributes = copy_math_attributes(font, source, clone) targets[tag].setdefault(cp, []).append(clone) mapping.append( { 'source_id': sid, 'feature': tag, 'unicode': f'U+{cp:04X}', 'source_tag': family, 'source_slot': slot, 'source_glyph': source, 'clone_glyph': clone, 'opentype_value': len(targets[tag][cp]), 'display_stage': len(targets[tag][cp]), 'fontspec_alternate': len(targets[tag][cp]) - 1, 'attributes': attributes, 'source_gid': original.index(source), 'clone_gid': len(original) + len(mapping) } ) # Only cv17 gets an isolated vertical construction. Its leaf must # not re-use the raw source: that source is in # U+221A's ordinary next chain. for r in mapping: if r['feature'] != 'cv17': continue clone = r['clone_glyph'] leaf = clone + '.terminal' if leaf in cs.charStrings: raise ValueError('duplicate terminal leaf ' + leaf) source = r['source_glyph'] raw_bbox = bounds(font, source) cs.charStrings[leaf] = len(cs.charStringsIndex) cs.charStringsIndex.items.append(copy.deepcopy(cs[clone])) top.charset.append(leaf) font['hmtx'].metrics[leaf] = font['hmtx'][clone] attributes = copy_math_attributes(font, clone, leaf) advance = round(raw_bbox[3] - raw_bbox[1]) + 1 if ( clone in m.VertGlyphCoverage.glyphs or source not in ( v.VariantGlyph for c in m.VertGlyphConstruction for v in c.MathGlyphVariantRecord ) ): raise ValueError( 'cv17 ordinary source chain/clone changed ' + r['source_id'] ) con = ot.MathGlyphConstruction() con.GlyphAssembly = None vr = ot.MathGlyphVariantRecord() vr.VariantGlyph = leaf vr.AdvanceMeasurement = advance con.MathGlyphVariantRecord = [vr] con.VariantCount = 1 m.VertGlyphCoverage.glyphs.append(clone) m.VertGlyphConstruction.append(con) m.VertGlyphCount = len(m.VertGlyphCoverage.glyphs) terminals.append( { 'source_id': r['source_id'], 'clone_glyph': clone, 'leaf_glyph': leaf, 'clone_gid': r['clone_gid'], 'leaf_gid': len(original) + len(mapping) + len(terminals), 'advance_measurement': advance, 'source_bbox': list(raw_bbox), 'hmtx': list(font['hmtx'][clone]), 'attributes': attributes } ) font.setGlyphOrder( original + [r['clone_glyph'] for r in mapping] + [r['leaf_glyph'] for r in terminals] ) font['maxp'].numGlyphs = len(font.getGlyphOrder()) font['hhea'].numberOfHMetrics = len(font.getGlyphOrder()) # cv20 roots are stretchable constructions, never fixed clones. for cp in (9182, 9183): root = f"brace.plain.{('over' if cp == 9182 else 'under')}" if root not in m.HorizGlyphCoverage.glyphs: raise ValueError('missing cv20 horizontal construction ' + root) targets['cv20'][cp] = [root] automatic = [] for cp, slot, expected in ( (9182, 176, 'uni23DE.curly.exe176'), (9183, 144, 'uni23DF.curly.exe144') ): root = imports.get(('exe', slot)) con = cons.get(root) if ( root != expected or font.getBestCmap().get(cp) != f'uni{cp:04X}' or (not con) or (con.VariantCount != 35) or (len(con.MathGlyphVariantRecord) != 35) or (not con.GlyphAssembly) or (len(con.GlyphAssembly.PartRecords) != 5) or ( [con.MathGlyphVariantRecord[i].AdvanceMeasurement for i in (0, -1)] != [1201, 29601] ) ): raise ValueError(f'cv21 source construction mismatch U+{cp:04X}') targets['cv21'][cp] = [root] automatic.append( { 'feature': 'cv21', 'unicode': f'U+{cp:04X}', 'source_tag': 'exe', 'source_slot': slot, 'source_glyph': root, 'opentype_value': 1, 'fontspec_alternate': 0, 'source_gid': original.index(root), 'variant_count': 35, 'assembly_parts': 5 } ) for tag, by_cp in targets.items(): sub = ot.AlternateSubst() sub.alternates = { font.getBestCmap()[cp]: forms for cp, forms in by_cp.items() } if tag in SCRIPT_FIXED_INPUTS: add_script_fixed_inputs(font, tag, sub.alternates) if tag == 'cv18': # The existing ssty lookup precedes these selector lookups. # Its script accent glyphs still represent the same Unicode # inputs; route them to the same immutable fixed designs, # without moving any historical lookup or modifying the # original ssty behavior. for cp, forms in by_cp.items(): root = font.getBestCmap()[cp] for level in (1, 2): styled = f'{root}.ssty{level}' if styled not in original: raise ValueError('missing script accent ' + styled) sub.alternates[styled] = forms lookup = ot.Lookup() lookup.LookupType = 3 lookup.LookupFlag = 0 lookup.SubTable = [sub] lookup.SubTableCount = 1 gsub.LookupList.Lookup.append(lookup) rec = ot.FeatureRecord() rec.FeatureTag = tag rec.Feature = ot.Feature() rec.Feature.FeatureParams = None rec.Feature.LookupListIndex = [len(gsub.LookupList.Lookup) - 1] rec.Feature.LookupCount = 1 gsub.FeatureList.FeatureRecord.append(rec) gsub.LookupList.LookupCount = len(gsub.LookupList.Lookup) previous = list(gsub.FeatureList.FeatureRecord) gsub.FeatureList.FeatureRecord.sort(key=lambda r: r.FeatureTag) gsub.FeatureList.FeatureCount = len(previous) remap = { i: gsub.FeatureList.FeatureRecord.index(r) for i, r in enumerate(previous) } for script in gsub.ScriptList.ScriptRecord: for ls in [script.Script.DefaultLangSys] + [ r.LangSys for r in script.Script.LangSysRecord ]: if ls is None: continue ls.FeatureIndex = sorted( [remap[i] for i in ls.FeatureIndex] + [ remap[i] for i in range(len(old_features), len(previous)) ] ) ls.FeatureCount = len(ls.FeatureIndex) if ls.ReqFeatureIndex != 65535: ls.ReqFeatureIndex = remap[ls.ReqFeatureIndex] return { 'profile': 'source', 'edition': inventory['edition'], 'input_hashes': inventory['input_hashes'], 'original_glyph_count': len(original), 'clone_count': len(mapping), 'terminal_leaf_count': len(terminals), 'glyph_count': len(font.getGlyphOrder()), 'cv19_translations': translations, 'cv17_terminals': terminals, 'ordinary_brace_changes': root_changes, 'automatic_selectors': automatic, 'mapping': mapping } def edit(font, inventory): """Install selectors and update the average in the caller-owned font.""" result = install(font, inventory) update_average_width(font) return result