Files

190 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())