"""Structure-specific grid design; no runtime rasterizer changes.""" from __future__ import annotations import json from pathlib import Path CLASSES = { "enclosure": "口日目田由甲申回国园圈围", "left_right": "站铁路湖树谢桥城明林河村", "stacked": "高花草重量喜音意章营售票", "diagonal": "人入大文交永木水火公交父", "dense": "鹤警藏赢鬻醐圜鲴醴曲瞒圈", "latin": "0O1Il5S8B2Z6G9gMWmw", } def parse_bdf(path: Path, width: int = 8, height: int = 8, ascent: int = 7) -> dict[int, list[int]]: result = {} codepoint = 0 bounds = (0, 0, 0, 0) bitmap = False rows = [] for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if line.startswith("ENCODING "): codepoint = int(line.split()[1]) elif line.startswith("BBX "): bounds = tuple(map(int, line.split()[1:])) elif line == "BITMAP": rows, bitmap = [], True elif line == "ENDCHAR": bw, bh, bx, by = bounds grid = [0] * height top = ascent - bh - by for y, row in enumerate(rows): padding = ((bw + 7) // 8) * 8 - width + bx value = row >> padding if padding >= 0 else row << -padding if not 0 <= top + y < height or value >= 1 << width: raise ValueError(f"BDF outside {width}x{height}: U+{codepoint:04X}") grid[top + y] = value result[codepoint] = grid bitmap = False elif bitmap: rows.append(int(line, 16)) return result def pixel_rows(grid): return [sum((1 << (len(row) - 1 - x)) for x, bit in enumerate(row) if bit) for row in grid] def grid_rows(rows, width): return [[bool(row & (1 << (width - 1 - x))) for x in range(width)] for row in rows] def design_large(rows, *, dense=False, diagonal=False): native = grid_rows(rows, 16) out = [[False] * 16 for _ in range(32)] for y in range(16): for x in range(15): if not native[y][x]: continue out[2 * y][x] = True vertical = (y > 0 and native[y - 1][x]) or (y < 15 and native[y + 1][x]) # Dense horizontal bars are one row tall, with vertical stems # retained on the intervening row. Other strokes use two rows. out[2 * y + 1][x] = not dense or vertical or sum(native[y]) < 7 isolated = (x == 0 or not native[y][x - 1]) and not native[y][x + 1] if vertical and isolated: if x < 14 and not native[y][x + 1] and (x == 13 or not native[y][x + 2]): out[2 * y][x + 1] = True out[2 * y + 1][x + 1] = True elif x > 1 and not native[y][x - 1] and not native[y][x - 2]: out[2 * y][x - 1] = True out[2 * y + 1][x - 1] = True if diagonal and y < 15: # Use an offset step on the intermediate row rather than # a uniform two-row stair; the crossing point stays narrow. for dx in (-1, 1): nx = x + dx if 0 <= nx < 15 and native[y + 1][nx] and not native[y + 1][x]: out[2 * y + 1][nx] = True return pixel_rows(out) def enclosure(char, width, height): grid = [[False] * width for _ in range(height)] thick = 2 if height == 32 else 1 left, right = (1, 13) if height == 32 else (0, 6) top, bottom = (3, 28) if height == 32 else (1, 14) if height == 16 and char in "田由甲申": top, bottom = 2, 12 # leave room for the distinguishing top/bottom stems def box(x1, y1, x2, y2): for y in range(y1, y2 + 1): for x in range(x1, x2 + 1): grid[y][x] = True box(left, top, right, top + thick - 1) box(left, bottom - thick + 1, right, bottom) box(left, top, left + thick - 1, bottom) box(right - thick + 1, top, right, bottom) bars = {"日": [.5], "目": [.33, .67], "田": [.5], "由": [.5], "甲": [.5], "申": [.5]} for fraction in bars.get(char, []): y = round(top + (bottom - top) * fraction) box(left + thick, y, right - thick, y + thick - 1) if char in "田由甲申": middle = width // 2 - 1 box(middle, 0 if char in "由申" else top + thick, middle + thick - 1, height - 2 if char in "甲申" else bottom - thick) if char == "回": inset = 4 if height == 32 else 2 box(left + inset, top + inset, right - inset, top + inset + thick - 1) box(left + inset, bottom - inset - thick + 1, right - inset, bottom - inset) box(left + inset, top + inset, left + inset + thick - 1, bottom - inset) box(right - inset - thick + 1, top + inset, right - inset, bottom - inset) return pixel_rows(grid) def latin_large(rows): # Deliberately separate Latin design: 5-column skeleton expanded to 10 # columns, horizontal stems 2 rows, vertical stems fill the pitch gaps. source = grid_rows(rows, 8) skeleton = [source[y][1:6] for y in range(0, 14, 2)] grid = [[False] * 16 for _ in range(32)] for y, row in enumerate(skeleton): yy = 2 + y * 4 for x, bit in enumerate(row): if not bit: continue xx = 3 + x * 2 for dy in (0, 1): for dx in (0, 1): grid[yy + dy][xx + dx] = True if y < 6 and skeleton[y + 1][x]: for dy in (2, 3): for dx in (0, 1): grid[yy + dy][xx + dx] = True elif y < 6: for nx in (x - 1, x + 1): if 0 <= nx < 5 and skeleton[y + 1][nx]: for dy in (2, 3): grid[yy + dy][xx + (1 if nx > x else 0)] = True return pixel_rows(grid) def latin_lower(rows): native = grid_rows(rows, 16) grid = [[False] * 16 for _ in range(32)] for y in range(16): for x in range(8): if native[y][x]: # Reserve a common side bearing and use the ten-column Latin # body rather than leaving lowercase stranded at the left. left = 3 + x * 10 // 8 right = 3 + (x + 1) * 10 // 8 for xx in range(left, right): grid[2 * y][xx] = grid[2 * y + 1][xx] = True return pixel_rows(grid) def generate_large(source_dir: Path, small_dir: Path, required: set[int]): native = parse_bdf(source_dir / "upstream/unifont-18.0.01.bdf", 16, 16, 14) missing = required - native.keys() if missing: raise ValueError(f"Unifont missing target codepoints: {sorted(missing)}") masters = {} revisions = {} for cp in sorted(required): rows = design_large(native[cp], dense=sum(row.bit_count() for row in native[cp]) > 115) masters[f"{cp:04X}"] = [f"{row:04X}" for row in rows] for kind, characters in CLASSES.items(): if kind == "latin": continue for char in characters: key = f"{ord(char):04X}" if ord(char) not in required: continue rows = (enclosure(char, 16, 32) if char in "口日目田由甲申回" else design_large(native[ord(char)], dense=kind == "dense", diagonal=kind == "diagonal")) revisions[key] = {"rows": [f"{row:04X}" for row in rows], "class": kind, "method": "grid-redraw" if char in "口日目田由甲申回" else "structure-rule", "reason": {"enclosure": "重排包围框及内部留白。", "left_right": "分别增粗独立竖画,保留部件间空列。", "stacked": "横画保留双行厚度,竖画避开相邻笔画。", "diagonal": "重排斜画阶梯及交叉点。", "dense": "密集横画改单行,保留竖画连续及部件空隙。"}[kind]} small_revisions = json.loads((small_dir / "revisions.json").read_text(encoding="utf-8")) for key, revision in small_revisions.items(): cp = int(key, 16) if 48 <= cp <= 57 or 65 <= cp <= 90 or chr(cp) in "il": rows = latin_large([int(row, 16) for row in revision["rows"]]) revisions[key] = {"rows": [f"{row:04X}" for row in rows], "class": "latin", "method": "grid-redraw", "reason": "独立重排拉丁骨架,双列主笔画及26px字身,区分易混字符。"} for cp in range(ord("a"), ord("z") + 1): if chr(cp) not in "il": revisions[f"{cp:04X}"] = {"rows": [f"{row:04X}" for row in latin_lower(native[cp])], "class": "latin", "method": "structure-rule", "reason": "小写独立分配十列字身及侧边距,保留基线和下伸部。"} for filename, data in [("glyphs.json", masters), ("revisions.json", revisions), ("classes.json", {kind: list(chars) for kind, chars in CLASSES.items()})]: (source_dir / filename).write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def improve_small(source_dir: Path): revisions = json.loads((source_dir / "revisions.json").read_text(encoding="utf-8")) masters = json.loads((source_dir / "glyphs.json").read_text(encoding="utf-8")) for kind in ("dense", "diagonal"): for char in CLASSES[kind]: key = f"{ord(char):04X}" rows = [int(row, 16) for row in masters[key]] for y in range(1, 15, 2): previous, following = rows[y - 1], rows[y + 1] if kind == "dense": common = previous & following interior = common & (common << 1) & (common >> 1) rows[y] &= ~interior elif previous.bit_count() <= 4 and following.bit_count() <= 4: rows[y] |= previous & ((following << 1) | (following >> 1)) revisions[key] = {"rows": [f"{row:02X}" for row in rows], "class": kind, "method": "structure-rule", "reason": "密集处减少连续实心块,保留原横画与边缘竖画。" if kind == "dense" else "对稀疏撇捺补独立阶梯连接,保留交叉辨识。"} for char in "口日目田由甲申回": revisions[f"{ord(char):04X}"] = {"rows": [f"{row:02X}" for row in enclosure(char, 8, 16)], "class": "enclosure", "method": "grid-redraw", "reason": "使用16行重新安排包围框、内横画及贯穿竖画,避免仅扩展8行骨架。"} (source_dir / "revisions.json").write_text(json.dumps(revisions, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")