- Rebuild CJK font subset (1421 glyphs) and convert CFF→TTF for stb_truetype compatibility, fixing Chinese/Japanese/Korean rendering - Add force quit confirmation dialog with cancel/confirm actions - Show force quit tooltip immediately on hover (no delay) - Translate hardcoded English strings in settings dropdowns (auto-lock timeouts, slider "Off" labels) - Fix mojibake en-dashes in 7 translation JSON files - Add helper scripts: build_cjk_subset, convert_cjk_to_ttf, check_font_coverage, fix_mojibake
215 lines
6.4 KiB
Python
215 lines
6.4 KiB
Python
#!/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)
|