Add bundled bus fonts and shared font favorites
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user