"""Prepare compatible upright weight masters from the approved static endpoints.""" from bisect import bisect_right from contextlib import ExitStack import hashlib from itertools import permutations import json from pathlib import Path import tomllib import fontTools from fontTools.misc.bezierTools import calcQuadraticArcLength from fontTools.pens.basePen import BasePen from fontTools.pens.ttGlyphPen import TTGlyphPen from fontTools.ttLib import TTFont from fontTools.ttLib.tables._g_l_y_f import flagOverlapSimple as OVERLAP_SIMPLE ROOT = Path(__file__).resolve().parent.parent CONFIG = tomllib.loads((ROOT / 'build-plan.toml').read_text()) ARROWS = {ord(char) for char in CONFIG['terminal']['arrows']} CELL_GRAPHICS = {cp for first, last in CONFIG['terminal']['ranges'] for cp in range(first, last + 1)} MASTERS = (('ExtraLight', 200), ('Regular', 400), ('Bold', 700), ('ExtraBold', 800)) STATIC_STYLES = (('Medium', 500), ('SemiBold', 600)) def split(segment, t): a = segment[0] + (segment[1] - segment[0]) * t if len(segment) == 2: return (segment[0], a), (a, segment[1]) b = segment[1] + (segment[2] - segment[1]) * t m = a + (b - a) * t return (segment[0], a, m), (m, b, segment[2]) def length(segment): if len(segment) == 2: return abs(segment[1] - segment[0]) return calcQuadraticArcLength(*[(p.real, p.imag) for p in segment]) def parameter_at_distance(segment, distance): lo, hi = 0.0, 1.0 for _ in range(18): t = (lo + hi) / 2 if length(split(segment, t)[0]) < distance: lo = t else: hi = t return (lo + hi) / 2 class Contours(BasePen): def __init__(self, glyph_set): super().__init__(glyph_set) self.contours = [] def _moveTo(self, p): self.start = self.current = complex(*p) self.segments = [] def _lineTo(self, p): p = complex(*p) if p != self.current: self.segments.append((self.current, p)) self.current = p def _qCurveToOne(self, control, p): p = complex(*p) self.segments.append((self.current, complex(*control), p)) self.current = p def _closePath(self): if self.current != self.start: self.segments.append((self.current, self.start)) self.contours.append(self.segments) def canonical_contour(segments): # An actual extremum avoids arbitrary Boolean contour starts without changing winding. candidates = [] for i, segment in enumerate(segments): candidates.append((segment[0], i, 0)) if len(segment) == 3: a, b, c = (p.imag for p in segment) if a - 2 * b + c: t = (a - b) / (a - 2 * b + c) if 0 < t < 1: candidates.append((split(segment, t)[0][-1], i, t)) peak = min(candidates, key=lambda item: (-item[0].imag, item[0].real)) span = max(point.real for point, _, _ in candidates) - min(point.real for point, _, _ in candidates) # Rounding can swap separated accent tips, but round tops need their true extremum. ties = [item for item in candidates if peak[0].imag - item[0].imag <= 1 and abs(item[0].real - peak[0].real) > span / 2] _, i, t = min([peak, *ties], key=lambda item: item[0].real) if t: before, after = split(segments[i], t) return [after] + segments[i + 1:] + segments[:i] + [before] return segments[i:] + segments[:i] def contour_parameterization(segments): lengths = [length(segment) for segment in segments] total = sum(lengths) if total <= 0: raise ValueError('Degenerate contour') knots = [0] for size in lengths: knots.append(knots[-1] + size / total) knots[-1] = 1 return lengths, knots def contour_samples(segments, lengths, knots): samples = [] for u in (i / 64 for i in range(64)): i = min(bisect_right(knots, u) - 1, len(segments) - 1) fraction = (u - knots[i]) / (knots[i + 1] - knots[i]) t = parameter_at_distance(segments[i], fraction * lengths[i]) samples.append(split(segments[i], t)[0][-1]) return samples def compatible_glyphs(contours, reference=0, align_phase=False): contours = [[canonical_contour(c) for c in master] for master in contours] if len({len(master) for master in contours}) != 1: raise ValueError('Masters have different contour topology') parameters = [[contour_parameterization(c) for c in master] for master in contours] samples = [[contour_samples(c, *p) for c, p in zip(master, params)] for master, params in zip(contours, parameters)] count = len(contours[0]) for master in sorted(range(len(contours)), key=lambda i: abs(i - reference)): if master == reference: continue order = min(permutations(range(count)), key=lambda order: sum( abs(a - b) ** 2 for i, j in enumerate(order) for a, b in zip(samples[reference][i], samples[master][j]))) contours[master] = [contours[master][i] for i in order] parameters[master] = [parameters[master][i] for i in order] if not align_phase: continue for index, source_index in enumerate(order): neighbor = master + 1 if master < reference else master - 1 target = contour_samples(contours[neighbor][index], *parameters[neighbor][index]) source = samples[master][source_index] phase = min(range(len(source)), key=lambda phase: sum( abs(a - source[(i + phase) % len(source)]) ** 2 for i, a in enumerate(target))) if not phase: continue segments = contours[master][index] lengths, knots = parameters[master][index] u = phase / len(source) i = min(bisect_right(knots, u) - 1, len(segments) - 1) t = parameter_at_distance(segments[i], (u - knots[i]) / (knots[i + 1] - knots[i]) * lengths[i]) if t: before, after = split(segments[i], t) segments = [after] + segments[i + 1:] + segments[:i] + [before] else: segments = segments[i:] + segments[:i] contours[master][index] = segments parameters[master][index] = contour_parameterization(segments) pens = [TTGlyphPen(None) for _ in contours] for outlines, params in zip(zip(*contours), zip(*parameters)): knots = sorted({u for _, source_knots in params for u in source_knots}) # Near-identical knots otherwise create sub-unit, rounding-only segments. knots = [u for i, u in enumerate(knots) if i == 0 or u - knots[i - 1] > 1e-8] for segments, (lengths, source_knots), pen in zip(outlines, params, pens): pen.moveTo((segments[0][0].real, segments[0][0].imag)) for lo, hi in zip(knots, knots[1:]): i = min(bisect_right(source_knots, (lo + hi) / 2) - 1, len(segments) - 1) ts = [] for u in (lo, hi): fraction = (u - source_knots[i]) / (source_knots[i + 1] - source_knots[i]) ts.append(0 if fraction <= 1e-8 else 1 if fraction >= 1 - 1e-8 else parameter_at_distance(segments[i], fraction * lengths[i])) start, end = ts section = split(segments[i], end)[0] if start: section = split(section, start / end)[1] control = section[1] if len(section) == 3 else (section[0] + section[-1]) / 2 point = section[-1] pen.qCurveTo((control.real, control.imag), (point.real, point.imag)) pen.closePath() glyphs = [pen.glyph() for pen in pens] signatures = [(tuple(g.endPtsOfContours), tuple(int(flag) & 1 for flag in g.flags)) for g in glyphs] if any(signature != signatures[0] for signature in signatures): raise ValueError('Curve subdivision produced different point structures') return glyphs def main(): output = ROOT / 'build/variable' output.mkdir(parents=True, exist_ok=True) paths = [ROOT / 'build/native' / f'iocgnMono-{style}.ttf' for style, _ in MASTERS] masters = [output / f'{style}.ttf' for style, _ in MASTERS] with ExitStack() as stack: fonts = [stack.enter_context(TTFont(path, recalcTimestamp=False)) for path in paths] if any(font.getGlyphOrder() != fonts[0].getGlyphOrder() for font in fonts): raise ValueError('Masters have different glyph order') sets = [font.getGlyphSet() for font in fonts] ascii_names = {name for cp, name in fonts[0].getBestCmap().items() if cp < 128} symbol_names = {name for cp, name in fonts[0].getBestCmap().items() if cp in CELL_GRAPHICS | ARROWS} for name in fonts[0].getGlyphOrder(): if fonts[0]['glyf'][name].isComposite(): components = [[component.glyphName for component in font['glyf'][name].components] for font in fonts] if any(parts != components[0] for parts in components): raise ValueError(f'{name}: masters have different components') continue if name in symbol_names: # Boolean outlines can rotate their start point as stroke widths change. for font in fonts: glyph = font['glyf'][name] overlap = glyph.flags[0] & OVERLAP_SIMPLE if glyph.numberOfContours else 0 start = 0 for end in getattr(glyph, 'endPtsOfContours', ()): points = list(glyph.coordinates[start:end + 1]) flags = glyph.flags[start:end + 1] pivot = min((i for i, flag in enumerate(flags) if flag & 1), key=lambda i: (-points[i][1], points[i][0])) glyph.coordinates[start:end + 1] = points[pivot:] + points[:pivot] glyph.flags[start:end + 1] = flags[pivot:] + flags[:pivot] start = end + 1 if overlap: # The overlap flag belongs to the glyph's first point, not to a contour's. for i in range(1, len(glyph.flags)): glyph.flags[i] &= 0xFF ^ OVERLAP_SIMPLE glyph.flags[0] |= OVERLAP_SIMPLE signatures = [(font['glyf'][name].numberOfContours, tuple(getattr(font['glyf'][name], 'endPtsOfContours', ())), tuple(int(flag) & 1 for flag in getattr(font['glyf'][name], 'flags', ()))) for font in fonts] if all(signature == signatures[0] for signature in signatures): continue outlines = [] for gs in sets: pen = Contours(gs) gs[name].draw(pen) outlines.append(pen.contours) try: # Preserve approved ASCII interpolation; new accents may need phase alignment. glyphs = compatible_glyphs(outlines, reference=1, align_phase=name not in ascii_names) except ValueError as error: raise ValueError(f'{name}: {error}') from error for font, glyph in zip(fonts, glyphs): font['glyf'][name] = glyph glyph.recalcBounds(font['glyf']) advance = font['hmtx'][name][0] font['hmtx'][name] = (advance, getattr(glyph, 'xMin', 0)) for font, path in zip(fonts, masters): for name in font.getGlyphOrder(): glyph = font['glyf'][name] if glyph.isComposite(): glyph.recalcBounds(font['glyf']) font['hmtx'][name] = (font['hmtx'][name][0], glyph.xMin) font.save(path) sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() manifest = {'weights': dict(MASTERS), 'advances': [0, 500], 'fontTools': fontTools.__version__, 'compatibility': 'Contour matching, top-extremum alignment, phase alignment for new Latin outlines and shared arc-length curve subdivision; no polygon tracing.', 'sources': {str(path.relative_to(ROOT)): sha(path) for path in [Path(__file__), *paths]}, 'masters': {path.name: sha(path) for path in masters}} (output / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') print('Prepared four compatible upright weight masters.') if __name__ == '__main__': main()