完成一个主界面开发流程

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
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
i1_to_png.py — 将 LVGL 模拟器 `--screenshot` 产出的 I1(1bpp) 原始位图转换为 PNG。
输入格式(模拟器 main.c 的 --screenshot 模式输出):
- 1 位/像素,每字节 MSB 在左(bit7=该行最左像素),bit=1 为前景、bit=0 为背景
- 每行 stride = 宽 / 8 字节,行内无填充
- 模拟器当前已剥离 8 字节调色板,文件即纯像素数据(宽/8 × 高 字节)
- 若遇到仍含调色板的旧文件(像素数据 + 8 字节),--auto-palette 会自动识别并跳过
用法示例:
python i1_to_png.py shot.bin # -> shot.png(默认 160x160)
python i1_to_png.py shot.bin -o out.png # 指定输出
python i1_to_png.py shot.bin --scale 6 # 放大 6 倍便于查看(最近邻,保持像素感)
python i1_to_png.py shot.bin -W 320 -H 240 # 非默认分辨率
python i1_to_png.py shot.bin --fg 0,0,0 --bg 255,255,255 # 自定义前/背景色(RGB 模式)
python i1_to_png.py shot.bin --invert # 前景/背景互换
依赖:Pillow(pip install pillow)。本仓库环境已验证 PIL 12.2.0。
退出码:0 成功;1 参数/数据错误。
"""
import argparse
import os
import sys
# DTU 屏幕默认分辨率(160x160,1bpp)
DEFAULT_W = 160
DEFAULT_H = 160
# I1 调色板大小(2 色 × 4 字节)。模拟器新版已剥离,旧文件可能残留。
I1_PALETTE_BYTES = 2 * 4
def _decode_i1(data, width, height):
"""把原始 I1 数据解成 (height, width) 的 0/1 矩阵。MSB 在左。"""
stride = width // 8
if width % 8 != 0:
raise ValueError("宽度必须是 8 的整数倍(I1 按字节打包),当前 %d" % width)
need = stride * height
if len(data) < need:
raise ValueError("数据不足:%d 字节 < 需要 %d 字节(%dx%d I1)"
% (len(data), need, width, height))
px = []
for y in range(height):
base = y * stride
row = [0] * width
for x in range(width):
byte_i = x >> 3
bit_i = 7 - (x & 7)
row[x] = (data[base + byte_i] >> bit_i) & 1
px.append(row)
return px
def _pick_palette_offset(data, width, height, auto):
"""返回像素数据在文件中的起始偏移(跳过调色板)。"""
stride = width // 8
need = stride * height
sizes = [len(data)]
if auto:
# 像素数据前可能带 I1 调色板(8 字节)
if len(data) == need + I1_PALETTE_BYTES:
return I1_PALETTE_BYTES
if len(data) < need:
raise ValueError("数据不足:%d 字节 < 像素所需 %d 字节" % (len(data), need))
return 0
def i1_to_png(input_path, output_path=None, width=DEFAULT_W, height=DEFAULT_H,
scale=1, invert=False, auto_palette=True,
fg=None, bg=None):
"""核心转换函数,可被其他脚本直接 import 调用。
fg / bg 形如 (r,g,b);为 None 时用黑白(L 模式)。
返回输出文件路径。
"""
from PIL import Image
if not os.path.isfile(input_path):
raise FileNotFoundError(input_path)
with open(input_path, "rb") as f:
data = f.read()
off = _pick_palette_offset(data, width, height, auto_palette)
px = _decode_i1(data[off:], width, height)
nw, nh = width, height
if fg is None and bg is None:
# 灰度:bit=1 -> 白(255),bit=0 -> 黑(0)
img = Image.new("L", (nw, nh))
ipx = img.load()
for y in range(nh):
for x in range(nw):
bit = px[y][x]
if invert:
bit ^= 1
ipx[x, y] = 255 if bit else 0
else:
fgc = fg if fg is not None else (255, 255, 255)
bgc = bg if bg is not None else (0, 0, 0)
if invert:
fgc, bgc = bgc, fgc
img = Image.new("RGB", (nw, nh), bgc)
ipx = img.load()
for y in range(nh):
for x in range(nw):
bit = px[y][x]
ipx[x, y] = fgc if bit else bgc
# 放大(最近邻,保持 1bpp 像素感)
if scale and scale > 1:
img = img.resize((nw * scale, nh * scale), Image.NEAREST)
if output_path is None:
output_path = os.path.splitext(input_path)[0] + ".png"
img.save(output_path, "PNG")
return output_path
def _parse_rgb(s):
if s is None:
return None
parts = s.split(",")
if len(parts) != 3:
raise argparse.ArgumentTypeError("颜色格式应为 R,G,B,如 255,0,0")
try:
r, g, b = (int(p) for p in parts)
except ValueError:
raise argparse.ArgumentTypeError("颜色分量必须是整数")
for v in (r, g, b):
if not (0 <= v <= 255):
raise argparse.ArgumentTypeError("颜色分量需在 0~255")
return (r, g, b)
def _ensure_utf8_console():
"""Windows 控制台默认 GBK,中文会乱码;统一切到 UTF-8(不支持则静默跳过)。"""
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8")
except (AttributeError, ValueError):
pass
def main(argv=None):
_ensure_utf8_console()
ap = argparse.ArgumentParser(
description="将模拟器 I1(1bpp) 原始截图转换为 PNG",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
ap.add_argument("input", help="I1 原始位图文件(.bin)")
ap.add_argument("-o", "--output", default=None,
help="输出 PNG 路径(默认与输入同名 .png)")
ap.add_argument("-W", "--width", type=int, default=DEFAULT_W, help="图像宽度")
ap.add_argument("-H", "--height", type=int, default=DEFAULT_H, help="图像高度")
ap.add_argument("--scale", type=int, default=1,
help="放大倍数(最近邻,便于查看小图)")
ap.add_argument("--invert", action="store_true", help="前景/背景互换")
ap.add_argument("--no-auto-palette", action="store_true",
help="不做调色板自动识别(按纯像素数据解析)")
ap.add_argument("--fg", type=_parse_rgb, default=None, help="前景色 R,G,B(bit=1)")
ap.add_argument("--bg", type=_parse_rgb, default=None, help="背景色 R,G,B(bit=0)")
args = ap.parse_args(argv)
try:
out = i1_to_png(
args.input,
output_path=args.output,
width=args.width,
height=args.height,
scale=args.scale,
invert=args.invert,
auto_palette=not args.no_auto_palette,
fg=args.fg,
bg=args.bg,
)
except (ValueError, FileNotFoundError) as e:
print("错误:%s" % e, file=sys.stderr)
return 1
print("已生成:%s" % out)
return 0
if __name__ == "__main__":
sys.exit(main())