157 lines
7.6 KiB
Python
157 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""从保留的抽签源 PNG 拆分共享图层、五个签文,生成 LZ4;--check 不写文件。
|
|
|
|
会覆盖 lcd_layers/chouqian 和生成 C;先保存手工美术编辑。背景杂点归零、
|
|
清除背面残留钓线、抽取并共享正面;RGB565+A8 编码往返无损不等于拆分原画无损。
|
|
"""
|
|
import argparse
|
|
from collections import deque
|
|
import json
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
from compress_idle_expression import pixels565
|
|
|
|
def compress(raw):
|
|
"""LZ4 block:字面量和 16-bit 回指;编码器自包含,不下载库。"""
|
|
out=bytearray();table={};anchor=pos=0
|
|
def length(value):
|
|
while value>=255: out.append(255);value-=255
|
|
out.append(value)
|
|
while pos+12<=len(raw):
|
|
key=raw[pos:pos+4];previous=table.get(key);table[key]=pos
|
|
if previous is None or pos-previous>65535:
|
|
pos+=1;continue
|
|
size=4
|
|
while pos+size<len(raw)-5 and raw[previous+size]==raw[pos+size]: size+=1
|
|
literals=pos-anchor;match=size-4
|
|
out.append((min(literals,15)<<4)|min(match,15))
|
|
if literals>=15: length(literals-15)
|
|
out.extend(raw[anchor:pos]);offset=pos-previous;out.extend((offset&255,offset>>8))
|
|
if match>=15: length(match-15)
|
|
end=pos+size
|
|
for p in range(pos+1,end):
|
|
if p+4<=len(raw):table[raw[p:p+4]]=p
|
|
pos=anchor=end
|
|
literals=len(raw)-anchor;out.append(min(literals,15)<<4)
|
|
if literals>=15:length(literals-15)
|
|
out.extend(raw[anchor:])
|
|
assert decompress(out)==raw
|
|
return out
|
|
|
|
def decompress(data):
|
|
out=bytearray();pos=0
|
|
def length(value):
|
|
nonlocal pos
|
|
if value==15:
|
|
while True:
|
|
v=data[pos];pos+=1;value+=v
|
|
if v!=255:break
|
|
return value
|
|
while pos<len(data):
|
|
token=data[pos];pos+=1;literals=length(token>>4)
|
|
out.extend(data[pos:pos+literals]);pos+=literals
|
|
if pos==len(data):break
|
|
offset=data[pos]|data[pos+1]<<8;pos+=2;match=length(token&15)+4
|
|
assert 0<offset<=len(out)
|
|
for _ in range(match):out.append(out[-offset])
|
|
return bytes(out)
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SOURCE = ROOT / "components/platform/assets/lcd_source"
|
|
LAYERS = ROOT / "components/platform/assets/lcd_layers/chouqian"
|
|
GENERATED = ROOT / "components/platform/src/generated/emote_gfx/fortune_assets.c"
|
|
RESULTS = ("buyun", "heping", "shangshang", "xiaoji", "zhongshang")
|
|
|
|
def clean(name, box, largest=False):
|
|
image = Image.open(SOURCE / name).convert("RGBA").crop(box)
|
|
pixels = list(image.getdata())
|
|
candidates = {(i % image.width, i // image.width) for i, (r,g,b,a) in enumerate(pixels)
|
|
if max(r,g,b)>35 and a}
|
|
components=[]
|
|
while candidates:
|
|
seed=candidates.pop(); queue=deque([seed]); group={seed}
|
|
while queue:
|
|
x,y=queue.popleft()
|
|
for nx in range(max(0,x-1),min(image.width,x+2)):
|
|
for ny in range(max(0,y-1),min(image.height,y+2)):
|
|
if (nx,ny) in candidates:
|
|
candidates.remove((nx,ny));group.add((nx,ny));queue.append((nx,ny))
|
|
if len(group)>=8: components.append(group)
|
|
selected=max(components,key=len) if largest else set().union(*components)
|
|
image.putdata([(r,g,b,a) if (i%image.width,i//image.width) in selected else (0,0,0,0)
|
|
for i,(r,g,b,a) in enumerate(pixels)])
|
|
return image
|
|
|
|
def text_layer(name):
|
|
image=Image.open(SOURCE/f"chouqian_{name}2.png").convert("RGBA").crop((136,94,184,198))
|
|
# 只保留原签文橙红笔画及边缘,不把黄色签底一起叠加。
|
|
image.putdata([(r,g,b,a) if r>150 and g<175 and b<100 and r>g*1.35 else (0,0,0,0)
|
|
for r,g,b,a in image.getdata()])
|
|
assert image.getbbox(), name
|
|
return image
|
|
|
|
def layers():
|
|
pile=clean("chouqian_qianzhi1.png",(56,164,264,240))
|
|
back=clean("chouqian_qianzhi1.png",(130,37,193,150),True)
|
|
for y in range(18,32):
|
|
for x in range(8):
|
|
r,g,b,a=back.getpixel((x,y))
|
|
if r>g*1.3 and g>b*1.5: back.putpixel((x,y),(0,0,0,0))
|
|
edge=clean("chouqian_qianzhi2.png",(139,38,180,148),True)
|
|
front=clean("chouqian_shangshang2.png",(110,24,210,214),True)
|
|
# 原签文所在内部签底做逐行双色插值;边框、肉垫、外轮廓保持原图。
|
|
for y in range(70,174):
|
|
l=front.getpixel((26,y));r=front.getpixel((73,y))
|
|
assert l[3] and r[3], y
|
|
for x in range(26,74):
|
|
f=(x-26)/47
|
|
front.putpixel((x,y),tuple(round(l[c]*(1-f)+r[c]*f) for c in range(3))+(255,))
|
|
question=Image.open(SOURCE/"chouqian_shangshang0.png").convert("RGBA").crop((147,83,174,133))
|
|
question.putdata([(r,g,b,a) if r>150 and g<215 and b<170 and r>g*1.10 else (0,0,0,0)
|
|
for r,g,b,a in question.getdata()])
|
|
assert question.getbbox()
|
|
rod=clean("chouqian_qianzhi0.png",(0,40,169,96))
|
|
return [("pile",pile,56,164),("back",back,0,0),("edge",edge,0,0),
|
|
("front",front,0,0),("question",question,0,0),("rod",rod,0,40)] + [
|
|
("text_"+name,text_layer(name),0,0) for name in RESULTS]
|
|
|
|
def main():
|
|
args=argparse.ArgumentParser();args.add_argument("--check",action="store_true");args=args.parse_args()
|
|
items=layers(); out=['/* Generated by tools/prepare_fortune_expression.py; do not edit. */',
|
|
'#include "lcd_emote_fortune.h"','']
|
|
entries=[]; report=[]
|
|
if not args.check: LAYERS.mkdir(parents=True,exist_ok=True)
|
|
for name,image,x,y in items:
|
|
pixels=pixels565(image)
|
|
raw=bytes(v for hi,lo,a in pixels for v in (hi,lo))+bytes(a for hi,lo,a in pixels)
|
|
data=compress(raw) # 对 RGB565 高低字节与独立 A8 平面逐字节往返检查。
|
|
out += [f'static const uint8_t fortune_{name}[] = {{']
|
|
out += [' '+', '.join(f'0x{v:02x}' for v in data[i:i+24])+',' for i in range(0,len(data),24)]
|
|
out += ['};','']
|
|
entries.append(f' {{{image.width}, {image.height}, {x}, {y}, sizeof(fortune_{name}), fortune_{name}}},')
|
|
report.append(dict(name=name,width=image.width,height=image.height,x=x,y=y,
|
|
raw_bytes=image.width*image.height*3,lz4_bytes=len(data)))
|
|
if args.check:
|
|
with Image.open(LAYERS/f"{name}.png") as old:
|
|
assert old.convert("RGBA").tobytes()==image.tobytes(), name
|
|
else: image.save(LAYERS/f"{name}.png",optimize=True)
|
|
out += ['const lcd_fortune_asset_t lcd_fortune_shared[LCD_FORTUNE_SHARED_COUNT] = {',*entries[:6],'};',
|
|
'const lcd_fortune_asset_t lcd_fortune_text[LCD_FORTUNE_RESULT_COUNT] = {',*entries[6:],'};',
|
|
'const char *const lcd_fortune_result_dirs[LCD_FORTUNE_RESULT_COUNT] = {',
|
|
' '+', '.join('"chouqian_'+s+'"' for s in RESULTS)+',','};','']
|
|
source='\n'.join(out)
|
|
metadata=json.dumps(dict(canvas=[320,240],window=[56,20,208,220],layers=report,
|
|
note="黑背景归零;共享爪子,签文字形来自原图;旧 PNG 保留"),ensure_ascii=False,indent=2)+'\n'
|
|
if args.check:
|
|
assert GENERATED.read_text()==source
|
|
assert (LAYERS/"layout.json").read_text()==metadata
|
|
else:
|
|
GENERATED.write_text(source);(LAYERS/"layout.json").write_text(metadata)
|
|
total=sum(s["lz4_bytes"] for s in report)
|
|
peak=208*220*2+sum(s["raw_bytes"] for s in report[:6])+max(s["raw_bytes"] for s in report[6:])
|
|
assert total<70*1024, ("为代码/描述保留10 KiB预算",total)
|
|
assert peak<300*1024,peak
|
|
print(json.dumps(dict(layers=report,payload_bytes=total,pixel_psram_peak=peak),ensure_ascii=False,indent=2))
|
|
|
|
if __name__=="__main__": main()
|