完成一个主界面开发流程

This commit is contained in:
2026-09-03 23:08:16 +08:00
parent 52d7dc7d4d
commit 89462d8aa1
21 changed files with 2177 additions and 198 deletions
+52
View File
@@ -59,6 +59,57 @@ def resolve_font(path):
sys.exit('ERROR: 未找到 C:\\Windows\\Fonts\\simsun.ttc,请用 --font 指定字体 ttf/otf 路径')
# ---------------------------------------------------------------------------
# 箭头字形合并
# 宋体(simsun.ttc)没有 U+25B6(▶)字形,lv_font_conv 会静默跳过 → 真机显示成方框。
# 该字形从 Segoe UI Symbol(seguisym.ttf,Windows 自带)提取,全量重生成字体后
# 自动补回,避免"重生成字体箭头又变方框"。
# ---------------------------------------------------------------------------
ARROW_CP_DEC = 9654 # U+25B6 ▶
ARROW_BITMAP = [0x80, 0xe0, 0xf0, 0xfc, 0xff, 0xfe, 0xf8, 0xf0, 0xc0, 0x0]
ARROW_ADV_W, ARROW_BOX_W, ARROW_BOX_H, ARROW_OFS_X, ARROW_OFS_Y = 165, 8, 10, 1, -1
def merge_arrow_glyph(font_path):
"""若字体缺 U+25B6(▶),把预提取的箭头字形合并进去(位图 + glyph_dsc + cmap)。"""
import re
raw = open(font_path, 'rb').read()
has_bom = raw.startswith(b'\xef\xbb\xbf')
src = raw.decode('utf-8-sig')
if re.search(r'\.range_start\s*=\s*9654\s*,', src):
print('arrow U+25B6 已存在,跳过合并')
return
# 当前位图总字节数(新字形 bitmap_index 的取值)
bm = re.search(r'glyph_bitmap\[\]\s*=\s*\{', src)
end_bm = src.find('\n};', bm.end())
body_bm = re.sub(r'/\*.*?\*/', '', src[bm.end():end_bm], flags=re.S)
total = len(re.findall(r'0x[0-9a-fA-F]+', body_bm))
glyph_count = len(re.findall(r'\.bitmap_index\s*=\s*\d+', src))
# 1) 追加位图
bmp = ',\n\n /* U+25B6 "▶"(来源:Segoe UI Symbol;宋体无此字形) */\n ' + \
', '.join('0x%02x' % b for b in ARROW_BITMAP)
src = src[:end_bm] + bmp + src[end_bm:]
# 2) 追加 glyph_dsc
gd = re.search(r'glyph_dsc\[\]\s*=\s*\{', src)
end_gd = src.find('\n};', gd.end())
entry = ',\n {.bitmap_index = %d, .adv_w = %d, .box_w = %d, .box_h = %d, .ofs_x = %d, .ofs_y = %d} /* U+25B6 */' % (
total, ARROW_ADV_W, ARROW_BOX_W, ARROW_BOX_H, ARROW_OFS_X, ARROW_OFS_Y)
src = src[:end_gd] + entry + src[end_gd:]
# 3) 追加 cmap(单码位映射)
cm = re.search(r'cmaps\[\]\s*=\s*\{', src)
end_cm = src.find('\n};', cm.end())
cmap = ',\n {\n .range_start = %d, .range_length = 1, .glyph_id_start = %d,\n' \
' .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY\n }' % (
ARROW_CP_DEC, glyph_count)
src = src[:end_cm] + cmap + src[end_cm:]
# 4) cmap_num +1
src = re.sub(r'\.cmap_num = (\d+),',
lambda m: '.cmap_num = %d,' % (int(m.group(1)) + 1), src, count=1)
out = b'\xef\xbb\xbf' if has_bom else b''
open(font_path, 'wb').write(out + src.encode('utf-8'))
print('已合并箭头 U+25B6:位图+%d 字节,glyph_id=%d' % (len(ARROW_BITMAP), glyph_count))
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--bpp', type=int, default=1, help='字体位深 1/2/3(默认 1)')
@@ -86,6 +137,7 @@ def main():
r = subprocess.run(cmd)
if r.returncode != 0:
sys.exit('FAILED rc=%d' % r.returncode)
merge_arrow_glyph(OUT) # 补回宋体缺失的 ▶ 字形(幂等)
print('OK %d bytes -> %s' % (os.path.getsize(OUT), OUT))
+77
View File
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""
render_mockup.py — 把 mockup HTML 的 1:1 视图渲染成 160×160 PNG(无头 Chrome/Edge)
用法:
python tools/ui_mock/render_mockup.py <mockup.html> <out.png> [--pad 24] [--size 160]
原理:
mockup 的 body 有 padding(默认 24px),1:1 视图是第一个 flex 子项,位于 (pad, pad),
尺寸 160×160。用无头浏览器截整页,再裁出 (pad, pad, pad+160, pad+160) 即得纯页面位图。
浏览器抗锯齿会在边缘产生灰阶;对比前由 compare_mockup.py 统一二值化(阈值 128)。
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
from PIL import Image
import numpy as np
BROWSER_CANDIDATES = [
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
]
def find_browser():
for c in BROWSER_CANDIDATES:
if os.path.exists(c):
return c
sys.exit("未找到 Chrome/Edge(用于无头渲染)")
def render(html_path, out_png, pad=24, size=160):
browser = find_browser()
html_abs = Path(html_path).resolve()
if not html_abs.exists():
sys.exit("找不到 HTML: %s" % html_abs)
out_abs = Path(out_png).resolve()
out_abs.parent.mkdir(parents=True, exist_ok=True)
win = pad + size + pad + 12 # 留一点余量
cmd = [
browser,
"--headless",
"--disable-gpu",
"--hide-scrollbars",
"--force-device-scale-factor=1",
"--window-size=%d,%d" % (win, win),
"--screenshot=%s" % out_abs,
html_abs.as_uri(),
]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
if not out_abs.exists():
sys.exit("截图失败:\n%s\n%s" % (r.stdout, r.stderr))
img = Image.open(out_abs).convert("L")
box = (pad, pad, pad + size, pad + size)
page = img.crop(box)
arr = np.array(page)
dark = float((arr < 128).mean())
page.save(out_abs)
print("OK %s -> %s (%dx%d, 黑像素 %.1f%%)" % (html_abs, out_abs, size, size, dark * 100))
return out_abs
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("html")
ap.add_argument("out")
ap.add_argument("--pad", type=int, default=24, help="mockup body padding(1:1 视图左上偏移)")
ap.add_argument("--size", type=int, default=160)
a = ap.parse_args()
render(a.html, a.out, pad=a.pad, size=a.size)