add: LVGL Image Converter Script 增加LVGL图片转换工具脚本,可以自定义小智的表情文件。 (#423)

* add: LVGL Image Converter

* add: README

* Rename README.MD to README.md

* remove readme image

---------

Co-authored-by: Xiaoxia <terrence@tenclass.com>
This commit is contained in:
香草味的纳西妲
2025-04-01 06:51:11 +08:00
committed by GitHub
parent 31c1f07481
commit 7c6b39e65d
3 changed files with 1712 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
# LVGL图片转换工具
这个目录包含两个用于处理和转换图片为LVGL格式的Python脚本
## 1. LVGLImage (LVGLImage.py)
引用自LVGL[官方repo](https://github.com/lvgl/lvgl)的转换脚本[LVGLImage.py](https://github.com/lvgl/lvgl/blob/master/scripts/LVGLImage.py)
## 2. LVGL图片转换工具 (lvgl_tools_gui.py)
调用`LVGLImage.py`将图片批量转换为LVGL图片格式
可用于修改小智的默认表情,具体修改教程[在这里](https://www.bilibili.com/video/BV12FQkYeEJ3/)
### 特性
- 图形化操作,界面更友好
- 支持批量转换图片
- 自动识别图片格式并选择最佳的颜色格式转换
- 多分辨率支持
### 使用方法
安装Pillow
```bash
pip install Pillow # 处理图像需要
```
运行转换工具
```bash
python lvgl_tools_gui.py
```

View File

@@ -0,0 +1,253 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image
import os
import tempfile
import sys
from LVGLImage import LVGLImage, ColorFormat, CompressMethod
HELP_TEXT = """LVGL图片转换工具使用说明
1. 添加文件:点击“添加文件”按钮选择需要转换的图片,支持批量导入
2. 移除文件:在列表中选中文件前的复选框“[ ]”(选中后会变成“[√]”),点击“移除选中”可删除选定文件
3. 设置分辨率选择需要的分辨率如128x128
建议根据自己的设备的屏幕分辨率来选择。过大和过小都会影响显示效果。
4. 颜色格式:选择“自动识别”会根据图片是否透明自动选择,或手动指定
除非你了解这个选项,否则建议使用自动识别,不然可能会出现一些意想不到的问题……
5. 压缩方式选择NONE或RLE压缩
除非你了解这个选项否则建议保持默认NONE不压缩
6. 输出目录:设置转换后文件的保存路径
默认为程序所在目录下的output文件夹
7. 转换:点击“转换全部”或“转换选中”开始转换
"""
class ImageConverterApp:
def __init__(self, root):
self.root = root
self.root.title("LVGL图片转换工具")
self.root.geometry("750x650")
# 初始化变量
self.output_dir = tk.StringVar(value=os.path.abspath("output"))
self.resolution = tk.StringVar(value="128x128")
self.color_format = tk.StringVar(value="自动识别")
self.compress_method = tk.StringVar(value="NONE")
# 创建UI组件
self.create_widgets()
self.redirect_output()
def create_widgets(self):
# 参数设置框架
settings_frame = ttk.LabelFrame(self.root, text="转换设置")
settings_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew")
# 分辨率设置
ttk.Label(settings_frame, text="分辨率:").grid(row=0, column=0, padx=2)
ttk.Combobox(settings_frame, textvariable=self.resolution,
values=["128x128", "64x64", "32x32"], width=8).grid(row=0, column=1, padx=2)
# 颜色格式
ttk.Label(settings_frame, text="颜色格式:").grid(row=0, column=2, padx=2)
ttk.Combobox(settings_frame, textvariable=self.color_format,
values=["自动识别", "RGB565", "RGB565A8"], width=10).grid(row=0, column=3, padx=2)
# 压缩方式
ttk.Label(settings_frame, text="压缩方式:").grid(row=0, column=4, padx=2)
ttk.Combobox(settings_frame, textvariable=self.compress_method,
values=["NONE", "RLE"], width=8).grid(row=0, column=5, padx=2)
# 文件操作框架
file_frame = ttk.LabelFrame(self.root, text="输入文件")
file_frame.grid(row=1, column=0, padx=10, pady=5, sticky="nsew")
# 文件操作按钮
btn_frame = ttk.Frame(file_frame)
btn_frame.pack(fill=tk.X, pady=2)
ttk.Button(btn_frame, text="添加文件", command=self.select_files).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="移除选中", command=self.remove_selected).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="清空列表", command=self.clear_files).pack(side=tk.LEFT, padx=2)
# 文件列表Treeview
self.tree = ttk.Treeview(file_frame, columns=("selected", "filename"),
show="headings", height=10)
self.tree.heading("selected", text="选中", anchor=tk.W)
self.tree.heading("filename", text="文件名", anchor=tk.W)
self.tree.column("selected", width=60, anchor=tk.W)
self.tree.column("filename", width=600, anchor=tk.W)
self.tree.pack(fill=tk.BOTH, expand=True)
self.tree.bind("<ButtonRelease-1>", self.on_tree_click)
# 输出目录
output_frame = ttk.LabelFrame(self.root, text="输出目录")
output_frame.grid(row=2, column=0, padx=10, pady=5, sticky="ew")
ttk.Entry(output_frame, textvariable=self.output_dir, width=60).pack(side=tk.LEFT, padx=5)
ttk.Button(output_frame, text="浏览", command=self.select_output_dir).pack(side=tk.RIGHT, padx=5)
# 转换按钮和帮助按钮
convert_frame = ttk.Frame(self.root)
convert_frame.grid(row=3, column=0, padx=10, pady=10)
ttk.Button(convert_frame, text="转换全部文件", command=lambda: self.start_conversion(True)).pack(side=tk.LEFT, padx=5)
ttk.Button(convert_frame, text="转换选中文件", command=lambda: self.start_conversion(False)).pack(side=tk.LEFT, padx=5)
ttk.Button(convert_frame, text="帮助", command=self.show_help).pack(side=tk.RIGHT, padx=5)
# 日志区域(新增清空按钮部分)
log_frame = ttk.LabelFrame(self.root, text="日志")
log_frame.grid(row=4, column=0, padx=10, pady=5, sticky="nsew")
# 添加按钮框架
log_btn_frame = ttk.Frame(log_frame)
log_btn_frame.pack(fill=tk.X, side=tk.BOTTOM)
# 清空日志按钮
ttk.Button(log_btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.RIGHT, padx=5, pady=2)
self.log_text = tk.Text(log_frame, height=15)
self.log_text.pack(fill=tk.BOTH, expand=True)
# 布局配置
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(1, weight=1)
self.root.rowconfigure(4, weight=1)
def clear_log(self):
"""清空日志内容"""
self.log_text.delete(1.0, tk.END)
def show_help(self):
messagebox.showinfo("帮助", HELP_TEXT)
def redirect_output(self):
class StdoutRedirector:
def __init__(self, text_widget):
self.text_widget = text_widget
self.original_stdout = sys.stdout
def write(self, message):
self.text_widget.insert(tk.END, message)
self.text_widget.see(tk.END)
self.original_stdout.write(message)
def flush(self):
self.original_stdout.flush()
sys.stdout = StdoutRedirector(self.log_text)
def on_tree_click(self, event):
region = self.tree.identify("region", event.x, event.y)
if region == "cell":
col = self.tree.identify_column(event.x)
item = self.tree.identify_row(event.y)
if col == "#1": # 点击的是选中列
current_val = self.tree.item(item, "values")[0]
new_val = "[√]" if current_val == "[ ]" else "[ ]"
self.tree.item(item, values=(new_val, self.tree.item(item, "values")[1]))
def select_output_dir(self):
path = filedialog.askdirectory()
if path:
self.output_dir.set(path)
def select_files(self):
files = filedialog.askopenfilenames(filetypes=[("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif")])
for f in files:
self.tree.insert("", tk.END, values=("[ ]", os.path.basename(f)), tags=(f,))
def remove_selected(self):
to_remove = []
for item in self.tree.get_children():
if self.tree.item(item, "values")[0] == "[√]":
to_remove.append(item)
for item in reversed(to_remove):
self.tree.delete(item)
def clear_files(self):
for item in self.tree.get_children():
self.tree.delete(item)
def start_conversion(self, convert_all):
input_files = [
self.tree.item(item, "tags")[0]
for item in self.tree.get_children()
if convert_all or self.tree.item(item, "values")[0] == "[√]"
]
if not input_files:
msg = "没有找到可转换的文件" if convert_all else "没有选中任何文件"
messagebox.showwarning("警告", msg)
return
os.makedirs(self.output_dir.get(), exist_ok=True)
# 解析转换参数
width, height = map(int, self.resolution.get().split('x'))
compress = CompressMethod.RLE if self.compress_method.get() == "RLE" else CompressMethod.NONE
# 执行转换
self.convert_images(input_files, width, height, compress)
def convert_images(self, input_files, width, height, compress):
success_count = 0
total_files = len(input_files)
for idx, file_path in enumerate(input_files):
try:
print(f"正在处理: {os.path.basename(file_path)}")
with Image.open(file_path) as img:
# 调整图片大小
img = img.resize((width, height), Image.Resampling.LANCZOS)
# 处理颜色格式
color_format_str = self.color_format.get()
if color_format_str == "自动识别":
# 检测透明通道
has_alpha = img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info)
if has_alpha:
img = img.convert('RGBA')
cf = ColorFormat.RGB565A8
else:
img = img.convert('RGB')
cf = ColorFormat.RGB565
else:
if color_format_str == "RGB565A8":
img = img.convert('RGBA')
cf = ColorFormat.RGB565A8
else:
img = img.convert('RGB')
cf = ColorFormat.RGB565
# 保存调整后的图片
base_name = os.path.splitext(os.path.basename(file_path))[0]
output_image_path = os.path.join(self.output_dir.get(), f"{base_name}_{width}x{height}.png")
img.save(output_image_path, 'PNG')
# 创建临时文件
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
temp_path = tmpfile.name
img.save(temp_path, 'PNG')
# 转换为LVGL C数组
lvgl_img = LVGLImage().from_png(temp_path, cf=cf)
output_c_path = os.path.join(self.output_dir.get(), f"{base_name}.c")
lvgl_img.to_c_array(output_c_path, compress=compress)
success_count += 1
os.unlink(temp_path)
print(f"成功转换: {base_name}.c\n")
except Exception as e:
print(f"转换失败: {str(e)}\n")
print(f"转换完成! 成功 {success_count}/{total_files} 个文件\n")
if __name__ == "__main__":
root = tk.Tk()
app = ImageConverterApp(root)
root.mainloop()