Files
matrix-screen-controller/核桃派软件源代码/scripts/bus_font_design.py
T

223 lines
11 KiB
Python

"""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")