197 lines
6.6 KiB
Python
197 lines
6.6 KiB
Python
"""按 480x480 页面规格渲染预览图(真 MiSans 字体 + 真素材 + 圆形遮罩)。
|
|||
|
|
|
||
|
|
支持 kind: rect / circle / image / label
|
||
|
|
label 支持 align / vtop / vcenter / lh,以及 LVGL 的 recolor 写法 "#RRGGBB 文字#"
|
||
|
|
"""
|
||
|
|
import os, re, sys
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
from PIL import Image, ImageDraw, ImageFont
|
||
|
|
|
||
|
|
from paths import PROJ, INPUTS, FONTS, WORK
|
||
|
|
ROOT = PROJ
|
||
|
|
FONT_FILES = {'semibold': 'MiSans-Semibold.ttf', 'medium': 'MiSans-Medium.ttf',
|
||
|
|
'regular': 'MiSans-Regular.ttf'}
|
||
|
|
W = H = 480
|
||
|
|
_fc = {}
|
||
|
|
|
||
|
|
|
||
|
|
def _font_from_firmware(name):
|
||
|
|
"""从 inputs/target/vela_font.bin 里现取 MiSans(固件里就自带这几个 ttf)。"""
|
||
|
|
p = os.path.join(INPUTS, 'target', 'vela_font.bin')
|
||
|
|
if not os.path.exists(p):
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
from romfs import Romfs
|
||
|
|
r = Romfs(p)
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
for path, ino in r.entries():
|
||
|
|
if path == name:
|
||
|
|
d = os.path.join(WORK, 'fonts')
|
||
|
|
os.makedirs(d, exist_ok=True)
|
||
|
|
out = os.path.join(d, name)
|
||
|
|
open(out, 'wb').write(r.read(ino))
|
||
|
|
return out
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def font(weight, size):
|
||
|
|
k = (weight, size)
|
||
|
|
if k in _fc:
|
||
|
|
return _fc[k]
|
||
|
|
name = FONT_FILES.get(weight, FONT_FILES['medium'])
|
||
|
|
p = os.path.join(FONTS, name)
|
||
|
|
if not os.path.exists(p):
|
||
|
|
p = _font_from_firmware(name) or p
|
||
|
|
if not os.path.exists(p):
|
||
|
|
for alt in (r'C:\Windows\Fonts\msyh.ttc', r'C:\Windows\Fonts\msyhbd.ttc',
|
||
|
|
r'C:\Windows\Fonts\arial.ttf'):
|
||
|
|
if os.path.exists(alt):
|
||
|
|
p = alt
|
||
|
|
print(' [warn] %s 缺失,预览改用 %s(只影响预览缩略图)'
|
||
|
|
% (name, os.path.basename(alt)))
|
||
|
|
break
|
||
|
|
_fc[k] = ImageFont.truetype(p, size) if os.path.exists(p) else ImageFont.load_default()
|
||
|
|
return _fc[k]
|
||
|
|
|
||
|
|
|
||
|
|
def _rgb(c):
|
||
|
|
return tuple(int(c[i:i + 2], 16) for i in (0, 2, 4))
|
||
|
|
|
||
|
|
|
||
|
|
def split_recolor(text, base):
|
||
|
|
runs, buf, cur, incol = [], '', base, False
|
||
|
|
i = 0
|
||
|
|
while i < len(text):
|
||
|
|
if text[i] == '#':
|
||
|
|
if re.match(r'[0-9a-fA-F]{6}', text[i + 1:i + 7]):
|
||
|
|
runs.append((cur, buf)); buf = ''
|
||
|
|
cur = text[i + 1:i + 7].lower(); incol = True
|
||
|
|
i += 7
|
||
|
|
continue
|
||
|
|
if incol:
|
||
|
|
runs.append((cur, buf)); buf = ''
|
||
|
|
cur = base; incol = False
|
||
|
|
i += 1
|
||
|
|
continue
|
||
|
|
buf += text[i]; i += 1
|
||
|
|
runs.append((cur, buf))
|
||
|
|
return runs
|
||
|
|
|
||
|
|
|
||
|
|
def wrap_runs(d, runs, f, maxw):
|
||
|
|
lines = [[]]
|
||
|
|
for color, chunk in runs:
|
||
|
|
for k, seg in enumerate(chunk.split('\n')):
|
||
|
|
if k:
|
||
|
|
lines.append([])
|
||
|
|
if not seg:
|
||
|
|
continue
|
||
|
|
cur = ''
|
||
|
|
for ch in seg:
|
||
|
|
if d.textlength(cur + ch, font=f) > maxw and cur:
|
||
|
|
lines[-1].append((color, cur))
|
||
|
|
lines.append([])
|
||
|
|
cur = ch
|
||
|
|
else:
|
||
|
|
cur += ch
|
||
|
|
if cur:
|
||
|
|
lines[-1].append((color, cur))
|
||
|
|
return lines
|
||
|
|
|
||
|
|
|
||
|
|
OVERFLOW = [] # (文本前 14 字, lh, 行数, 块高, 框高)
|
||
|
|
|
||
|
|
|
||
|
|
def draw_label(img, d, it):
|
||
|
|
f = font(it.get('weight', 'medium'), it['size'])
|
||
|
|
runs = split_recolor(it['text'], it.get('color', 'ffffff')) \
|
||
|
|
if it.get('recolor') else [(it.get('color', 'ffffff'), it['text'])]
|
||
|
|
asc, desc = f.getmetrics()
|
||
|
|
natural = asc + desc
|
||
|
|
lh = max(it.get('lh', 0), natural)
|
||
|
|
lines = wrap_runs(d, runs, f, it['w'])
|
||
|
|
total = lh * len(lines)
|
||
|
|
if total > it['h'] + 0.5:
|
||
|
|
OVERFLOW.append((it['text'][:14].replace('\n', '/'), round(it.get('lh', 0), 2),
|
||
|
|
len(lines), round(total, 1), it['h'], round(natural, 2)))
|
||
|
|
if it.get('vcenter'):
|
||
|
|
y = it['y'] + (it['h'] - total) / 2
|
||
|
|
elif it.get('vtop'):
|
||
|
|
y = it['y']
|
||
|
|
else:
|
||
|
|
y = it['y'] + (it['h'] - total) / 2
|
||
|
|
for ln in lines:
|
||
|
|
wsum = sum(d.textlength(t, font=f) for _, t in ln)
|
||
|
|
if it.get('align') == 'center':
|
||
|
|
x = it['x'] + (it['w'] - wsum) / 2
|
||
|
|
elif it.get('align') == 'right':
|
||
|
|
x = it['x'] + it['w'] - wsum
|
||
|
|
else:
|
||
|
|
x = it['x']
|
||
|
|
for color, t in ln:
|
||
|
|
d.text((x, y), t, font=f, fill=_rgb(color) + (255,))
|
||
|
|
x += d.textlength(t, font=f)
|
||
|
|
y += lh
|
||
|
|
|
||
|
|
|
||
|
|
def render_raw(items):
|
||
|
|
"""画成 RGBA(不遮罩、不铺黑底)—— 切边检查就用这个。"""
|
||
|
|
img = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
||
|
|
d = ImageDraw.Draw(img)
|
||
|
|
for it in items:
|
||
|
|
k = it.get('kind')
|
||
|
|
if k == 'rect':
|
||
|
|
box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1]
|
||
|
|
r = it.get('radius', 0)
|
||
|
|
if r:
|
||
|
|
d.rounded_rectangle(box, radius=r, fill=_rgb(it['bg']) + (255,))
|
||
|
|
else:
|
||
|
|
d.rectangle(box, fill=_rgb(it['bg']) + (255,))
|
||
|
|
elif k == 'circle':
|
||
|
|
box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1]
|
||
|
|
d.ellipse(box, fill=_rgb(it['bg']) + (255,))
|
||
|
|
elif k == 'image':
|
||
|
|
p = it['src']
|
||
|
|
if os.path.exists(p):
|
||
|
|
im = Image.open(p).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS)
|
||
|
|
img.alpha_composite(im, (it['x'], it['y']))
|
||
|
|
elif k == 'label':
|
||
|
|
draw_label(img, d, it)
|
||
|
|
return img
|
||
|
|
|
||
|
|
|
||
|
|
def render(items, out, mask=True, ring=False):
|
||
|
|
img = render_raw(items)
|
||
|
|
if mask:
|
||
|
|
m = Image.new('L', (W, H), 0)
|
||
|
|
ImageDraw.Draw(m).ellipse([0, 0, W - 1, H - 1], fill=255)
|
||
|
|
img.putalpha(m)
|
||
|
|
base = Image.new('RGB', (W, H), (0, 0, 0))
|
||
|
|
base.paste(img, (0, 0), img)
|
||
|
|
if ring:
|
||
|
|
ImageDraw.Draw(base).ellipse([0, 0, W - 1, H - 1], outline=(80, 80, 90))
|
||
|
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||
|
|
base.save(out)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
from s5e_pages import PAGES
|
||
|
|
outs = []
|
||
|
|
for name, items in PAGES.items():
|
||
|
|
p = os.path.join(ROOT, 'docs', 'ui_%s.png' % name)
|
||
|
|
render(items, p)
|
||
|
|
outs.append(p)
|
||
|
|
print('wrote', p)
|
||
|
|
cols = 3
|
||
|
|
rows = (len(outs) + cols - 1) // cols
|
||
|
|
sheet = Image.new('RGB', (cols * (W + 12) + 12, rows * (H + 12) + 12), (28, 28, 32))
|
||
|
|
for i, p in enumerate(outs):
|
||
|
|
sheet.paste(Image.open(p), (12 + (i % cols) * (W + 12), 12 + (i // cols) * (H + 12)))
|
||
|
|
sp = os.path.join(ROOT, 'docs', 'ui_all.png')
|
||
|
|
sheet.save(sp)
|
||
|
|
print('wrote', sp, sheet.size)
|
||
|
|
if OVERFLOW:
|
||
|
|
print('!! 文本溢出 %d 处:%s' % (len(OVERFLOW), OVERFLOW))
|