"""Build a TrueType font from the cabinet's bitmap glyph atlases (font/_64.{xml,nutexb}). The cabinet ships no vector font: each language has its own BC7 atlas + XML (codepoint -> rect, no bearings, no kerning), and RenderText picks the atlas family by the cabinet language (fontType 0 rows -> jp) and the size/weight bucket per text record. YataiDON draws everything through one .ttf, so this merges the five 64pt atlases into one font: every codepoint takes its glyph from the first atlas (in --order) that has ink for it. Per atlas: scale = upm / fontPoint (jp_64: 16 units per pixel) advance = glyph width * scale (A: 51 px -> 816, same as the earlier build) baseline row = median bottom ink row of the Han/Hangul glyphs (+1), so the different atlases sit on one baseline; Latin-only atlases use 'A'. Tracing: alpha >= 128 at 4x bicubic upscale -> potrace -> cubic -> quadratic (cu2qu). usage: python build_font.py [--order jp,tw,cn,kr,en] [--jobs N] """ import sys, os, argparse, statistics, time import xml.etree.ElementTree as ET import numpy as np from PIL import Image import potrace from multiprocessing import Pool UPM = 1024 UPSCALE = 4 ASCENT, DESCENT = 880, -144 def parse_xml(path): r = ET.parse(path).getroot().find('font') glyphs = {int(x.get('index')): (int(x.get('offsetU')), int(x.get('offsetV')), int(x.get('width')), int(x.get('height'))) for x in r.findall('glyph')} return glyphs, {k: int(v) for k, v in r.attrib.items()} _A = None def _init(alpha_path): global _A _A = np.load(alpha_path, mmap_mode='r') def _trace(job): cp, u, v, w, h, scale, baseline_px = job a = np.asarray(_A[v:v + h, u:u + w]) rows = np.where(a.max(axis=1) >= 128)[0] if rows.size == 0: return cp, int(round(w * scale)), None # blank: advance only big = np.array(Image.fromarray(a).resize((w * UPSCALE, h * UPSCALE), Image.BICUBIC)) < 128 # potracer: True = white path = potrace.Bitmap(big).trace(turdsize=4, alphamax=1.0, opticurve=True, opttolerance=0.2) k = scale / UPSCALE def P(p): return (p.x * k, (baseline_px * UPSCALE - p.y) * k) contours = [] for curve in path.curves: segs = [('M', P(curve.start_point))] for s in curve.segments: if s.is_corner: segs.append(('L', P(s.c))); segs.append(('L', P(s.end_point))) else: segs.append(('C', P(s.c1), P(s.c2), P(s.end_point))) contours.append(segs) return cp, int(round(w * scale)), contours def main(): ap = argparse.ArgumentParser() ap.add_argument('atlas_dir'); ap.add_argument('out') ap.add_argument('--order', default='jp,tw,cn,kr,en') ap.add_argument('--han-order', default='', help='separate priority for CJK ideographs (U+2E80-9FFF, F900-FAFF, 20000+); --order then applies to everything else') ap.add_argument('--jobs', type=int, default=max(1, os.cpu_count() - 4)) ap.add_argument('--limit', type=int, default=0, help='debug: glyphs per atlas') ap.add_argument('--embolden', default='', help='lang=px: dilate that atlas alpha by px before tracing') args = ap.parse_args() langs = args.order.split(',') han_langs = args.han_order.split(',') if args.han_order else langs def is_han(cp): return 0x2E80 <= cp <= 0x9FFF or 0xF900 <= cp <= 0xFAFF or cp >= 0x20000 def rank(lang, cp): return (han_langs if is_han(cp) else langs).index(lang) if lang in (han_langs if is_han(cp) else langs) else 99 taken = {} # cp -> (advance, contours or None, lang) all_langs = list(dict.fromkeys(langs + han_langs)) for lang in all_langs: t0 = time.time() glyphs, attr = parse_xml(os.path.join(args.atlas_dir, f'{lang}_64.xml')) im = Image.open(os.path.join(args.atlas_dir, f'{lang}_64.dds')) A = np.array(im)[:, :, 3] emb = dict(kv.split('=') for kv in args.embolden.split(',') if kv) if lang in emb and int(emb[lang]) > 0: from scipy.ndimage import grey_dilation k = 2 * int(emb[lang]) + 1; A = grey_dilation(A, size=(k, k)) scale = UPM / attr['fontPoint'] # baseline: where the Han / Hangul glyphs sit (Latin-only atlas: 'A') bots = [] for cp, (u, v, w, h) in glyphs.items(): if 0x4E00 <= cp <= 0x9FFF or 0xAC00 <= cp <= 0xD7A3: r = np.where(A[v:v + h, u:u + w].max(axis=1) >= 128)[0] if r.size: bots.append(int(r.max())) if bots: baseline_px = statistics.median(bots) + 1 else: u, v, w, h = glyphs[0x41] r = np.where(A[v:v + h, u:u + w].max(axis=1) >= 128)[0] baseline_px = int(r.max()) + 1 + 2 * 64 / attr['fontPoint'] # 'A' sits ~2 jp-px above the Han bottom alpha_path = os.path.join(args.atlas_dir, f'_{lang}_alpha.npy') np.save(alpha_path, A) jobs = [(cp, u, v, w, h, scale, baseline_px) for cp, (u, v, w, h) in sorted(glyphs.items()) if (cp not in taken or taken[cp][1] is None and cp not in (0x20, 0x3000, 0xA0) or rank(lang, cp) < rank(taken[cp][2], cp))] if args.limit: jobs = jobs[:args.limit] got = 0 with Pool(args.jobs, initializer=_init, initargs=(alpha_path,)) as pool: for cp, adv, contours in pool.imap_unordered(_trace, jobs, chunksize=64): if contours is None: if cp in (0x20, 0x3000, 0xA0) and cp not in taken: taken[cp] = (adv, None, lang) continue if cp in taken and taken[cp][1] is not None and rank(lang, cp) >= rank(taken[cp][2], cp): continue taken[cp] = (adv, contours, lang); got += 1 os.remove(alpha_path) print(f'{lang}_64: fontPoint {attr["fontPoint"]} scale {scale:.2f} baseline {baseline_px:.1f}px ' f'-> {got} new glyphs ({time.time() - t0:.0f}s)', flush=True) # ---- assemble from fontTools.fontBuilder import FontBuilder from fontTools.pens.ttGlyphPen import TTGlyphPen from fontTools.pens.cu2quPen import Cu2QuPen order = ['.notdef'] + [(f'u{cp:05X}' if cp > 0xFFFF else f'uni{cp:04X}') for cp in sorted(taken)] fb = FontBuilder(UPM, isTTF=True) fb.setupGlyphOrder(order) cmap, glyf, hmtx = {}, {}, {} pen = TTGlyphPen(None); glyf['.notdef'] = pen.glyph(); hmtx['.notdef'] = (512, 0) for name, cp in zip(order[1:], sorted(taken)): adv, contours, _ = taken[cp] pen = TTGlyphPen(None) if contours: qpen = Cu2QuPen(pen, max_err=1.0, reverse_direction=False) for segs in contours: for s in segs: if s[0] == 'M': qpen.moveTo(s[1]) elif s[0] == 'L': qpen.lineTo(s[1]) else: qpen.curveTo(s[1], s[2], s[3]) qpen.closePath() g = pen.glyph() glyf[name] = g g.recalcBounds(glyf) if contours else None hmtx[name] = (adv, getattr(g, 'xMin', 0) if contours else 0) cmap[cp] = name fb.setupCharacterMap(cmap) fb.setupGlyf(glyf) fb.setupHorizontalMetrics(hmtx) fb.setupHorizontalHeader(ascent=ASCENT, descent=DESCENT) fb.setupNameTable({'familyName': 'YataiDON HSS Atlas', 'styleName': 'Regular', 'uniqueFontIdentifier': 'YataiDON HSS Atlas 3.000', 'fullName': 'YataiDON HSS Atlas Regular', 'version': 'Version 3.000', 'psName': 'YataiDONHSSAtlas-Regular'}) fb.setupOS2(sTypoAscender=ASCENT, sTypoDescender=DESCENT, sTypoLineGap=0, usWinAscent=ASCENT, usWinDescent=-DESCENT, fsType=0) fb.setupPost() fb.save(args.out) from collections import Counter print('saved', args.out, len(order), 'glyphs;', dict(Counter(v[2] for v in taken.values()))) if __name__ == '__main__': main()