"""Nerd Font symbol preparation and deterministic family merging.""" from __future__ import annotations import copy from dataclasses import dataclass import hashlib from math import hypot, sqrt from pathlib import Path import pathops from fontTools.ttLib import TTFont from fontTools.ttLib.tables._c_m_a_p import CmapSubtable from fontTools.pens.ttGlyphPen import TTGlyphPen from font_geometry import append_polygon, stroke_skeleton from font_metadata import rename_font from font_output import save_ttf from font_project import CELL_WIDTH, POWERLINE_OVERLAP as POWERLINE_EDGE_OVERLAP from font_terminal import TERMINAL_LIGHT_RULE from font_types import LineMetrics, NerdPayload from font_pipeline import ( INK_WIDTH, NERD_INK_WIDTH, OPTICAL_FIT_CELL_WIDTH, OPTICAL_FIT_LEFT, OPTICAL_FIT_RIGHT, _simple_transformed_glyph, apply_family_line_metrics, ) EXCLUDED_NERD_CODEPOINT_RANGES = ((0xF300, 0xF381),) EXPECTED_NERD_SOURCE_SHA256 = ( "abd6258264a7b79ed06fde4a834522b973e05527c4e73bbb0d1efdb6194faaf2" ) NERD_SOURCE_UPM = 2048 NERD_SOURCE_ASCENDER = 1638 NERD_SOURCE_DESCENDER = -410 NERD_MIN_RULE_THICKNESS = 72 _POWERLINE_PRESERVE_ALIGNMENTS = { 0xE0B4: "left", 0xE0B5: "left", 0xE0B6: "right", 0xE0B7: "right", 0xE0B8: "left", 0xE0B9: "left", 0xE0BA: "right", 0xE0BB: "right", 0xE0BC: "left", 0xE0BD: "left", 0xE0BE: "right", 0xE0BF: "right", 0xE0C0: "left", 0xE0C1: "left", 0xE0C2: "right", 0xE0C3: "right", 0xE0C4: "left", 0xE0C5: "right", 0xE0C6: "left", 0xE0C7: "right", 0xE0C8: "left", 0xE0CA: "right", 0xE0CC: "left", 0xE0CD: "left", 0xE0CE: "left", 0xE0CF: "center", 0xE0D0: "left", 0xE0D1: "left", 0xE0D2: "left", 0xE0D4: "right", 0xE0D6: "left", 0xE0D7: "right", } _POWERLINE_FULL_HEIGHT_CODEPOINTS = frozenset(range(0xE0B4, 0xE0B8)) _PROGRESS_STRETCH_RULES = { 0xEE00: ("right", 0.05), 0xEE01: ("center", 0.10), 0xEE02: ("left", 0.05), 0xEE03: ("right", 0.05), 0xEE04: ("center", 0.10), 0xEE05: ("left", 0.05), } @dataclass(frozen=True, slots=True) class _NerdFitProfile: """Optical box and joining behavior for one Nerd symbol.""" width: float height: float center_y: float preserve_aspect: bool = True align: str = "center" overlap: float = 0.0 max_aspect_ratio: float | None = None def _nerd_fit_profile( codepoint: int, source_name: str, line_metrics: LineMetrics, cap_height: int, ) -> _NerdFitProfile: """Choose a visual box without deforming ordinary pictograms.""" line_height = line_metrics.ascender - line_metrics.descender line_center = (line_metrics.ascender + line_metrics.descender) / 2 # U+E0A0–E0A3 are status icons, not joining separators. Stretching them # to the full typographic line box made a branch icon almost twice as tall # as the surrounding capitals. if ( source_name.startswith(("pl-", "ple-")) and not 0xE0A0 <= codepoint <= 0xE0A3 ): return _NerdFitProfile( width=CELL_WIDTH, height=line_height, center_y=line_center, preserve_aspect=( codepoint not in _POWERLINE_FULL_HEIGHT_CODEPOINTS ), align=_POWERLINE_PRESERVE_ALIGNMENTS.get(codepoint, "center"), ) if source_name.startswith("extra-progress_") and 0xEE00 <= codepoint <= 0xEE05: stretch_rule = _PROGRESS_STRETCH_RULES.get(codepoint) if stretch_rule is not None: align, overlap = stretch_rule return _NerdFitProfile( width=CELL_WIDTH * (1 + overlap), height=line_height, center_y=line_center, preserve_aspect=False, align=align, overlap=overlap, ) # Ordinary icons use one shared source-cell transform in # ``load_nerd_payload``. This return remains a defensive fallback for # callers of the low-level fitting helper. return _NerdFitProfile( width=INK_WIDTH, height=INK_WIDTH, center_y=cap_height / 2, ) def _draw_powerline_round_cap( codepoint: int, line_metrics: LineMetrics, ) -> object: """Draw circular Powerline end caps on the terminal's physical grid. The upstream extra-symbol glyphs are drawn for a 2048u square source cell. Tight-bbox fitting them into Ilta's 620 × 1240 terminal cell made the curves subtly elliptical, left a flat shoulder at each attachment, and rounded away the source's seam overlap. Ilta's cell is exactly half as wide as its line box, so a native 620u-radius semicircle is both simpler and geometrically exact. """ if codepoint not in {0xE0B4, 0xE0B5, 0xE0B6, 0xE0B7}: raise ValueError(f"unsupported round Powerline cap U+{codepoint:04X}") bottom = float(line_metrics.descender) top = float(line_metrics.ascender) middle = (bottom + top) / 2 radius = (top - bottom) / 2 if abs(radius - CELL_WIDTH) > 0.5: raise ValueError( "round Powerline caps require a cell exactly half the line box: " f"{CELL_WIDTH}u cell versus {top - bottom}u line" ) bulges_right = codepoint in {0xE0B4, 0xE0B5} filled = codepoint in {0xE0B4, 0xE0B6} direction = 1.0 if bulges_right else -1.0 attachment = 0.0 if bulges_right else float(CELL_WIDTH) diagonal = radius / sqrt(2) control = radius * (sqrt(2) - 1) def x(offset: float) -> float: return attachment + direction * offset pen = TTGlyphPen(None) pen.moveTo((attachment, top)) pen.qCurveTo( (x(control), top), (x(diagonal), middle + diagonal), ) pen.qCurveTo( (x(radius), middle + control), (x(radius), middle), ) pen.qCurveTo( (x(radius), middle - control), (x(diagonal), middle - diagonal), ) pen.qCurveTo( (x(control), bottom), (attachment, bottom), ) if filled: # Overlap only the square attachment seam. The circular outer edge # remains exactly on the opposite cell boundary. seam = attachment - direction * POWERLINE_EDGE_OVERLAP pen.lineTo((seam, bottom)) pen.lineTo((seam, top)) else: rule = TERMINAL_LIGHT_RULE inner_radius = radius - rule inner_diagonal = inner_radius / sqrt(2) inner_control = inner_radius * (sqrt(2) - 1) pen.lineTo((attachment, bottom + rule)) pen.qCurveTo( (x(inner_control), bottom + rule), (x(inner_diagonal), middle - inner_diagonal), ) pen.qCurveTo( (x(inner_radius), middle - inner_control), (x(inner_radius), middle), ) pen.qCurveTo( (x(inner_radius), middle + inner_control), (x(inner_diagonal), middle + inner_diagonal), ) pen.qCurveTo( (x(inner_control), top - rule), (attachment, top - rule), ) pen.closePath() return pen.glyph() def _draw_powerline_core( codepoint: int, line_metrics: LineMetrics, ) -> object: """Draw canonical Powerline dividers and round caps in the target cell.""" if codepoint in {0xE0B4, 0xE0B5, 0xE0B6, 0xE0B7}: return _draw_powerline_round_cap(codepoint, line_metrics) bottom = float(line_metrics.descender) top = float(line_metrics.ascender) middle = (bottom + top) / 2 # One output-pixel-ish overlap at the intended 160 px proof size closes # background seams without the former 4.5% horizontal overshoot. overlap = POWERLINE_EDGE_OVERLAP if codepoint in {0xE0B0, 0xE0B2}: pen = TTGlyphPen(None) if codepoint == 0xE0B0: points = [ (-overlap, bottom), (-overlap, top), (CELL_WIDTH + overlap, middle), ] else: points = [ (CELL_WIDTH + overlap, bottom), (CELL_WIDTH + overlap, top), (-overlap, middle), ] append_polygon(pen, points) return pen.glyph() if codepoint not in {0xE0B1, 0xE0B3}: raise ValueError(f"unsupported native Powerline core U+{codepoint:04X}") rule = max(NERD_MIN_RULE_THICKNESS, 76) half_rule = rule / 2 half_rise = (top - bottom) / 2 # The soft separator is a stroked, mitred chevron rather than a filled # triangle. Its skeleton therefore has to sit inside the requested edge # bounds: these anchors make the 76u stroke's control box land exactly at # -8..CELL_WIDTH+8 instead of letting the middle miter overshoot by 21u. outer_x = 20.0 middle_x = CELL_WIDTH - 44.0 horizontal_run = middle_x - outer_x vertical_inset = ( half_rule * horizontal_run / hypot(horizontal_run, half_rise) ) + 0.5 path = pathops.Path() if codepoint == 0xE0B1: path.moveTo(outer_x, bottom + vertical_inset) path.lineTo(middle_x, middle) path.lineTo(outer_x, top - vertical_inset) else: path.moveTo( CELL_WIDTH - outer_x, bottom + vertical_inset, ) path.lineTo(CELL_WIDTH - middle_x, middle) path.lineTo( CELL_WIDTH - outer_x, top - vertical_inset, ) return stroke_skeleton( path, rule, 0.0, join=pathops.LineJoin.MITER_JOIN, ) def _nerd_fit_transform( bounds, profile: _NerdFitProfile, ) -> tuple[float, float, float, float]: """Return x/y scales and offsets for a symbol's measured ink bounds.""" x_min, y_min, x_max, y_max = bounds source_width = x_max - x_min source_height = y_max - y_min if source_width <= 0 or source_height <= 0: return 1.0, 1.0, 0.0, 0.0 scale_x = profile.width / source_width scale_y = profile.height / source_height if profile.preserve_aspect: uniform_scale = min(scale_x, scale_y) scale_x = uniform_scale scale_y = uniform_scale elif profile.max_aspect_ratio is not None: output_ratio = source_width * scale_x / (source_height * scale_y) if output_ratio > profile.max_aspect_ratio: scale_x *= profile.max_aspect_ratio / output_ratio output_width = source_width * scale_x if profile.align == "left": target_x_min = -CELL_WIDTH * profile.overlap offset_x = target_x_min - scale_x * x_min elif profile.align == "right": target_x_max = CELL_WIDTH * (1 + profile.overlap) offset_x = target_x_max - scale_x * x_max elif profile.align == "center": offset_x = CELL_WIDTH / 2 - scale_x * (x_min + x_max) / 2 else: raise ValueError(f"unknown Nerd symbol alignment: {profile.align}") # Keep the calculation explicit even though output_width is only needed # for the alignment invariant; it documents the intended optical box and # guards against accidental zero-width profiles. if output_width <= 0: raise ValueError("Nerd symbol fitting produced a non-positive width") offset_y = profile.center_y - scale_y * (y_min + y_max) / 2 return scale_x, scale_y, offset_x, offset_y def _contain_ordinary_nerd_glyph_x( glyph, *, codepoint: int, source_name: str, ) -> object: """Translate a shared-map icon only when its transformed ink leaves.""" glyph.recalcBounds(None) if glyph.numberOfContours <= 0: return glyph output_width = glyph.xMax - glyph.xMin if output_width > OPTICAL_FIT_CELL_WIDTH: raise ValueError( f"ordinary Nerd symbol U+{codepoint:04X} {source_name} is " f"{output_width}u wide after the shared transform; " "cannot contain it in the preserved optical cell without scaling" ) shift_x = ( OPTICAL_FIT_LEFT - glyph.xMin if glyph.xMin < OPTICAL_FIT_LEFT else OPTICAL_FIT_RIGHT - glyph.xMax if glyph.xMax > OPTICAL_FIT_RIGHT else 0 ) if not shift_x: return glyph glyph.coordinates.translate((shift_x, 0)) glyph.recalcBounds(None) if ( glyph.xMin < OPTICAL_FIT_LEFT or glyph.xMax > OPTICAL_FIT_RIGHT ): raise AssertionError( f"ordinary Nerd containment failed for U+{codepoint:04X} " f"{source_name}: {glyph.xMin}..{glyph.xMax}" ) return glyph def load_nerd_payload( source_path: Path, line_metrics: LineMetrics, cap_height: int, ) -> NerdPayload: source_bytes = source_path.read_bytes() source_sha = hashlib.sha256(source_bytes).hexdigest() if source_sha != EXPECTED_NERD_SOURCE_SHA256: raise ValueError( f"Nerd symbols source SHA-256 mismatch for {source_path}: " f"expected {EXPECTED_NERD_SOURCE_SHA256}, got {source_sha}" ) source = TTFont(source_path, recalcBBoxes=True, recalcTimestamp=False) if source["head"].unitsPerEm != NERD_SOURCE_UPM: raise ValueError( "unexpected Nerd symbol UPM: " f"{source['head'].unitsPerEm}; expected {NERD_SOURCE_UPM}" ) if ( source["hhea"].ascent, source["hhea"].descent, ) != (NERD_SOURCE_ASCENDER, NERD_SOURCE_DESCENDER): raise ValueError( "unexpected Nerd symbol line metrics: " f"{source['hhea'].ascent}/{source['hhea'].descent}" ) glyph_set = source.getGlyphSet() source_cmap = source.getBestCmap() source_to_codepoints: dict[str, list[int]] = {} for codepoint, glyph_name in source_cmap.items(): source_to_codepoints.setdefault(glyph_name, []).append(codepoint) glyphs: dict[str, object] = {} codepoint_to_name: dict[int, str] = {} source_center_y = ( NERD_SOURCE_ASCENDER + NERD_SOURCE_DESCENDER ) / 2 ordinary_scale = NERD_INK_WIDTH / NERD_SOURCE_UPM ordinary_offset_x = (CELL_WIDTH - NERD_SOURCE_UPM * ordinary_scale) / 2 ordinary_offset_y = cap_height / 2 - source_center_y * ordinary_scale progress_scale = CELL_WIDTH / NERD_SOURCE_UPM progress_offset_y = cap_height / 2 - source_center_y * progress_scale for source_name, codepoints in sorted( source_to_codepoints.items(), key=lambda item: min(item[1]) ): first_codepoint = min(codepoints) output_name = f"nf_u{first_codepoint:04X}" # Use TrueType control bounds here, not only the mathematical curve # extrema. Off-curve points are part of the rasterizer's working box; # fitting against the tighter BoundsPen result can otherwise leak a # few units past the declared line metrics on ornate separators. source_glyph = source["glyf"][source_name] source_glyph.recalcBounds(source["glyf"]) bounds = ( None if source_glyph.numberOfContours == 0 else ( source_glyph.xMin, source_glyph.yMin, source_glyph.xMax, source_glyph.yMax, ) ) joining_powerline = ( source_name.startswith(("pl-", "ple-")) and not 0xE0A0 <= first_codepoint <= 0xE0A3 ) joining_progress = ( source_name.startswith("extra-progress_") and 0xEE00 <= first_codepoint <= 0xEE05 ) native_powerline = 0xE0B0 <= first_codepoint <= 0xE0B7 ordinary_symbol = False if native_powerline: glyphs[output_name] = _draw_powerline_core( first_codepoint, line_metrics, ) elif bounds is not None and joining_powerline: profile = _nerd_fit_profile( first_codepoint, source_name, line_metrics, cap_height, ) scale_x, scale_y, offset_x, offset_y = _nerd_fit_transform( bounds, profile, ) elif joining_progress: # These six pieces are one animation/rail system. One shared, # uniform map preserves their source overlap and constant stroke; # fitting each tight bbox separately made adjacent frames pulse. scale_x = scale_y = progress_scale offset_x = 0.0 offset_y = progress_offset_y else: ordinary_symbol = True # Symbols Nerd Font Mono already supplies a deliberate 2048-unit # coordinate system. Preserve it wholesale. Tight-bbox fitting # had inflated "small circle", degree, sort, moon-phase and # spinner glyphs by as much as 3.2x and erased their shared frames. scale_x = scale_y = ordinary_scale offset_x = ordinary_offset_x offset_y = ordinary_offset_y # Keep even extremely thin minimize/remove rules canonical. # Thickening only Y deformed four ordinary icons by 15–35%; # final symbol-aware autohinting is the correct place to stabilize # those sub-pixel strokes. if not native_powerline: transformed = _simple_transformed_glyph( glyph_set, source_name, (scale_x, 0, 0, scale_y, offset_x, offset_y), ) if ordinary_symbol: transformed = _contain_ordinary_nerd_glyph_x( transformed, codepoint=first_codepoint, source_name=source_name, ) glyphs[output_name] = transformed for codepoint in codepoints: codepoint_to_name[codepoint] = output_name source.close() return NerdPayload( glyphs=glyphs, codepoint_to_name=codepoint_to_name, source_sha256=source_sha, ) def _ensure_full_unicode_cmaps(font: TTFont) -> list[object]: format_12 = [ table for table in font["cmap"].tables if table.isUnicode() and table.format == 12 ] if format_12: return format_12 existing = font.getBestCmap().copy() created = [] for platform_id, encoding_id in ((0, 4), (3, 10)): table = CmapSubtable.newSubtable(12) table.platformID = platform_id table.platEncID = encoding_id table.language = 0 table.cmap = existing.copy() font["cmap"].tables.append(table) created.append(table) return created def merge_nerd_symbols( base_ttf_path: Path, output_path: Path, payload: NerdPayload, style: str, clipping_metrics: LineMetrics, typographic_metrics: LineMetrics | None = None, ) -> int: font = TTFont(base_ttf_path, recalcBBoxes=True, recalcTimestamp=False) occupied = set(font.getBestCmap()) accepted = { codepoint: glyph_name for codepoint, glyph_name in payload.codepoint_to_name.items() if codepoint not in occupied and not any( start <= codepoint <= end for start, end in EXCLUDED_NERD_CODEPOINT_RANGES ) } used_names = set(accepted.values()) for glyph_name in sorted(used_names, key=lambda name: int(name[4:], 16)): # Nerd Fonts ships these pictograms as one canonical outline set. # Blanket per-weight stroking merged holes/components in thousands of # icons and still failed to match the text family's colour. Keeping # identical geometry across weights preserves logos, spinner frames, # and tiny semantic details while fixed symbol fallback hinting makes # their editor-size rasters deterministic. font["glyf"][glyph_name] = copy.deepcopy(payload.glyphs[glyph_name]) font["hmtx"].metrics[glyph_name] = (CELL_WIDTH, 0) order = font["glyf"].glyphOrder font.setGlyphOrder(order) font["glyf"].glyphOrder = order font["maxp"].numGlyphs = len(order) full_unicode_tables = _ensure_full_unicode_cmaps(font) for table in font["cmap"].tables: if not table.isUnicode(): continue for codepoint, glyph_name in accepted.items(): if table.format != 4 or codepoint <= 0xFFFF: table.cmap[codepoint] = glyph_name for table in full_unicode_tables: table.cmap.update(accepted) rename_font(font, style, nerd=True) apply_family_line_metrics(font, clipping_metrics, typographic_metrics) try: font["OS/2"].recalcUnicodeRanges(font) except AttributeError: pass # Re-hint the complete merged font once. In particular this grid-fits the # tiny Powerline/progress rails that otherwise shimmer between one and two # antialiasing rows at 11–15 px. Let ttfautohint use its fixed symbol # fallback. Borrowing each style's Latin CVT made identical Nerd outlines # pulse non-monotonically between Thin, Regular, and Black. save_ttf( font, output_path, autohint=True, ) return len(accepted)