#!/usr/bin/env python3 """Convert CJK subset from CID-keyed CFF/OTF to TrueType/TTF. stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly, so we need glyf-based TrueType outlines instead. Two approaches: 1. Direct CFF->TTF conversion via cu2qu (fontTools) 2. Download NotoSansSC-Regular.ttf (already TTF) and re-subset This script tries approach 1 first, falls back to approach 2. """ import os import sys import json import glob SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(SCRIPT_DIR) FONT_DIR = os.path.join(PROJECT_ROOT, "res", "fonts") LANG_DIR = os.path.join(PROJECT_ROOT, "res", "lang") SRC_OTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.otf") DST_TTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.ttf") def get_needed_codepoints(): """Collect all unique codepoints from CJK translation files.""" codepoints = set() for lang_file in glob.glob(os.path.join(LANG_DIR, "*.json")): with open(lang_file, "r", encoding="utf-8") as f: data = json.load(f) for value in data.values(): if isinstance(value, str): for ch in value: cp = ord(ch) # Include CJK + Hangul + fullwidth + CJK symbols/kana if cp >= 0x2E80: codepoints.add(cp) return codepoints def convert_cff_to_ttf(): """Convert existing OTF/CFF font to TTF using fontTools cu2qu.""" from fontTools.ttLib import TTFont from fontTools.pens.cu2quPen import Cu2QuPen from fontTools.pens.ttGlyphPen import TTGlyphPen print(f"Loading {SRC_OTF}...") font = TTFont(SRC_OTF) # Verify it's CFF if "CFF " not in font: print("Font is not CFF, skipping conversion") return False cff = font["CFF "] top = cff.cff.topDictIndex[0] print(f"ROS: {getattr(top, 'ROS', None)}") print(f"CID-keyed: {getattr(top, 'FDSelect', None) is not None}") glyphOrder = font.getGlyphOrder() print(f"Glyphs: {len(glyphOrder)}") # Use fontTools' built-in otf2ttf if available try: from fontTools.otf2ttf import otf_to_ttf otf_to_ttf(font) font.save(DST_TTF) print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)") font.close() return True except ImportError: pass # Manual conversion using cu2qu print("Using manual CFF->TTF conversion with cu2qu...") from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.pointPen import SegmentToPointPen from fontTools import ttLib from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph import struct # Get glyph set glyphSet = font.getGlyphSet() # Create new glyf table from fontTools.ttLib import newTable glyf_table = newTable("glyf") glyf_table.glyphs = {} glyf_table.glyphOrder = glyphOrder loca_table = newTable("loca") max_error = 1.0 # em-units tolerance for cubic->quadratic for gname in glyphOrder: try: ttPen = TTGlyphPen(glyphSet) cu2quPen = Cu2QuPen(ttPen, max_err=max_error, reverse_direction=True) glyphSet[gname].draw(cu2quPen) glyf_table.glyphs[gname] = ttPen.glyph() except Exception as e: # Fallback: empty glyph glyf_table.glyphs[gname] = TTGlyph() # Replace CFF with glyf del font["CFF "] if "VORG" in font: del font["VORG"] font["glyf"] = glyf_table font["loca"] = loca_table # Add required tables for TTF # head table needs indexToLocFormat font["head"].indexToLocFormat = 1 # long format # Create maxp for TrueType if "maxp" in font: font["maxp"].version = 0x00010000 # Update sfntVersion font.sfntVersion = "\x00\x01\x00\x00" # TrueType font.save(DST_TTF) print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)") font.close() return True def download_and_subset(): """Download NotoSansSC-Regular.ttf and subset it.""" import urllib.request from fontTools.ttLib import TTFont from fontTools import subset # Google Fonts provides static TTF files url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf" # Actually, we want TTF. Let's try the variable font approach. # Or better: use google-fonts API for static TTF # NotoSansSC static TTF from Google Fonts CDN tmp_font = "/tmp/NotoSansSC-Regular.ttf" if not os.path.exists(tmp_font): print(f"Downloading NotoSansSC-Regular.ttf...") url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc" # This is a TTC (font collection), too large. # Use the OTF we already have and convert it. return False print(f"Using {tmp_font}") font = TTFont(tmp_font) cmap = font.getBestCmap() print(f"Source has {len(cmap)} cmap entries") needed = get_needed_codepoints() print(f"Need {len(needed)} CJK codepoints") # Subset subsetter = subset.Subsetter() subsetter.populate(unicodes=needed) subsetter.subset(font) font.save(DST_TTF) print(f"Saved: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)") font.close() return True def verify_result(): """Verify the output TTF has glyf outlines and correct characters.""" from fontTools.ttLib import TTFont font = TTFont(DST_TTF) cmap = font.getBestCmap() print(f"\n--- Verification ---") print(f"Format: {font.sfntVersion!r}") print(f"Has glyf: {'glyf' in font}") print(f"Has CFF: {'CFF ' in font}") print(f"Cmap entries: {len(cmap)}") # Check key characters test_chars = { "历": 0x5386, "史": 0x53F2, # Chinese: history "概": 0x6982, "述": 0x8FF0, # Chinese: overview "设": 0x8BBE, "置": 0x7F6E, # Chinese: settings } for name, cp in test_chars.items(): status = "YES" if cp in cmap else "NO" print(f" {name} (U+{cp:04X}): {status}") size = os.path.getsize(DST_TTF) print(f"File size: {size} bytes ({size/1024:.1f} KB)") font.close() if __name__ == "__main__": print("=== CJK Font CFF -> TTF Converter ===\n") if convert_cff_to_ttf(): verify_result() else: print("Direct conversion failed, trying download approach...") if download_and_subset(): verify_result() else: print("ERROR: Could not convert font") sys.exit(1)