97 lines
5.3 KiB
Python
97 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""生成 HUD 字形子集和无损调色板游程数据;无需运行时分配/解压缓存。
|
|
|
|
--import-zip 只导入明确列出的 40x20 PNG,不执行压缩包中的任何内容。
|
|
原 PNG 保留;优化 PNG 与生成 C 是可再生文件。--check 只校验不写入。
|
|
"""
|
|
import argparse
|
|
import io
|
|
import re
|
|
from pathlib import Path
|
|
from zipfile import ZipFile
|
|
from PIL import Image
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ASSETS = ROOT / 'components/platform/assets/status_hud'
|
|
OUTPUT = ROOT / 'components/platform/src/generated/lcd_status_assets.c'
|
|
NAMES = [f'battery_{n}.png' for n in (1, 25, 50, 75, 100)] + [
|
|
f'battery_charging_{n}.png' for n in (1, 25, 50, 75, 100)]
|
|
|
|
def array(name, data):
|
|
rows = [' ' + ', '.join(f'0x{n:02x}' for n in data[i:i+24]) + ','
|
|
for i in range(0, len(data), 24)]
|
|
return f'static const uint8_t {name}[] = {{\n' + '\n'.join(rows) + '\n};\n'
|
|
|
|
def generate(check=False):
|
|
font = (ROOT / 'managed_components/lvgl__lvgl/src/font/lv_font_montserrat_14.c').read_text()
|
|
assert '.bpp = 4,' in font and '.bitmap_format = 0,' in font
|
|
assert re.search(r'\.line_height\s*=\s*16\b', font)
|
|
assert re.search(r'\.base_line\s*=\s*3\b', font)
|
|
assert '.range_start = 32, .range_length = 95, .glyph_id_start = 1,' in font
|
|
block = re.search(r'glyph_bitmap\[\]\s*=\s*\{(.*?)\n};', font, re.S)[1]
|
|
block = re.sub(r'/\*.*?\*/', '', block, flags=re.S)
|
|
bitmap = bytes(int(n, 16) for n in re.findall(r'0x[0-9a-fA-F]+', block))
|
|
block = re.search(r'glyph_dsc\[\]\s*=\s*\{(.*?)\n};', font, re.S)[1]
|
|
glyphs = [dict((key, int(value)) for key, value in re.findall(r'\.(\w+)\s*=\s*(-?\d+)', row))
|
|
for row in re.findall(r'\{([^{}]+)\}', block)]
|
|
subset = bytearray(); descriptions = []
|
|
for c in '0123456789:-':
|
|
g = glyphs[ord(c)-32+1]
|
|
count = (g['box_w'] * g['box_h'] + 1) // 2
|
|
raw = bitmap[g['bitmap_index']:g['bitmap_index']+count]
|
|
assert len(raw) == count
|
|
descriptions.append(f" {{{len(subset)}, {(g['adv_w']+8)//16}, {g['box_w']}, {g['box_h']}, {g['ofs_x']}, {g['ofs_y']}}}, /* {c} */")
|
|
subset.extend(raw)
|
|
text = ('/* Generated by tools/prepare_status_hud_assets.py; do not edit.\n'
|
|
' * Font subset derived from LVGL Montserrat 14, Copyright (c) 2025 LVGL Kft.\n'
|
|
' * See components/platform/assets/status_hud/LVGL-LICENSE.txt. */\n'
|
|
'#include "lcd_status_assets.h"\n\n')
|
|
text += array('lcd_status_font_bitmap', subset).replace('static const', 'const')
|
|
text += 'const lcd_status_glyph_t lcd_status_font_glyphs[12] = {\n' + '\n'.join(descriptions) + '\n};\n'
|
|
payload = 0
|
|
for i, name in enumerate(NAMES):
|
|
source = (ASSETS / 'battery_source' / name).read_bytes()
|
|
image = Image.open(io.BytesIO(source)).convert('RGBA')
|
|
assert image.size == (40, 20), name
|
|
out = io.BytesIO(); image.save(out, format='PNG', optimize=True, compress_level=9)
|
|
optimized = min((source, out.getvalue()), key=len)
|
|
assert Image.open(io.BytesIO(optimized)).convert('RGBA').tobytes() == image.tobytes()
|
|
destination = ASSETS / 'battery_optimized' / name
|
|
if check:
|
|
assert destination.read_bytes() == optimized, name
|
|
else:
|
|
destination.parent.mkdir(parents=True, exist_ok=True); destination.write_bytes(optimized)
|
|
rgba = image.tobytes()
|
|
pixels = [(((rgba[j] >> 3) << 11) | ((rgba[j+1] >> 2) << 5) | (rgba[j+2] >> 3), rgba[j+3])
|
|
for j in range(0, len(rgba), 4)]
|
|
palette = list(dict.fromkeys(pixels)); assert len(palette) <= 256
|
|
indices = {p: n for n, p in enumerate(palette)}; runs = bytearray()
|
|
for p in pixels:
|
|
index = indices[p]
|
|
if runs and runs[-1] == index and runs[-2] < 255: runs[-2] += 1
|
|
else: runs.extend((1, index))
|
|
decoded = [palette[runs[j+1]] for j in range(0, len(runs), 2) for _ in range(runs[j])]
|
|
assert decoded == pixels, name
|
|
pal = bytes(v for color, alpha in palette for v in (color >> 8, color & 255, alpha))
|
|
text += array(f'battery_palette_{i}', pal) + array(f'battery_runs_{i}', runs)
|
|
payload += len(pal)+len(runs)
|
|
print(f'{name}: PNG {len(source)} -> {len(optimized)} B; indexed RLE {len(pal)+len(runs)} B')
|
|
text += 'const lcd_status_icon_t lcd_status_battery_icons[10] = {\n'
|
|
text += ''.join(f' {{battery_palette_{i}, battery_runs_{i}, sizeof(battery_runs_{i})}},\n' for i in range(10)) + '};\n'
|
|
if check: assert OUTPUT.read_text() == text
|
|
else: OUTPUT.parent.mkdir(parents=True, exist_ok=True); OUTPUT.write_text(text)
|
|
print(f'Font bitmap {len(subset)} B; battery payload {payload} B; no runtime pixel allocations')
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(); parser.add_argument('--import-zip', type=Path); parser.add_argument('--check', action='store_true')
|
|
args = parser.parse_args()
|
|
if args.import_zip:
|
|
assert not args.check
|
|
with ZipFile(args.import_zip) as archive:
|
|
for name in NAMES:
|
|
data = archive.read('out/40x20/' + name)
|
|
image = Image.open(io.BytesIO(data)); assert image.size == (40, 20)
|
|
path = ASSETS / 'battery_source' / name
|
|
path.parent.mkdir(parents=True, exist_ok=True); path.write_bytes(data)
|
|
generate(args.check)
|