This commit is contained in:
2026-07-01 13:52:35 +08:00
commit fb65367654

573
gif_to_eye_image.py Normal file
View File

@@ -0,0 +1,573 @@
from pathlib import Path
import argparse
from itertools import groupby
import re
import python-rle
from PIL import Image, ImageSequence
DEFAULT_INPUT = Path(r"D:\esp\Project\lcd\source\eye2.bmp")
DEFAULT_OUTPUT_DIR = Path(r"D:\esp\Project\lcd\lcd\main")
DEFAULT_WIDTH = 320
DEFAULT_HEIGHT = 240
DEFAULT_INCLUDE_FRAMES = ""
DEFAULT_RLE_ITEMS_PER_LINE = 50
DEFAULT_FRAME_OUTPUT_DIR = Path(r"D:\esp\Project\lcd\source\frame")
DEFAULT_FRAME_FORMAT = "png"
PINYIN_MAP = {
"": "shang",
"": "zhong",
"": "qian",
"": "wu",
"": "xing",
"": "qian",
"": "zhi",
"": "he",
"": "ping",
"": "tu",
"": "xiao",
"": "ji",
"": "mu",
"": "yu",
"": "shui",
"": "huo",
"": "bu",
"": "yun",
"": "jin",
"": "dong",
"西": "xi",
"": "nan",
"": "bei",
"": "ban",
"": "duan",
"": "zhuan",
"": "quan",
"": "chou",
"": "zhen",
"": "duan",
"": "wang",
"": "geng",
"": "xin",
"": "you",
"": "fu",
"": "wu",
"": "qi",
"": "lian",
"": "lian",
"": "jie",
"": "shi",
"": "bai",
"": "dian",
"": "liang",
"": "di",
"": "zha",
"": "yan",
"": "dong",
"": "tu",
"": "zhao",
"": "cai",
"": "mao",
"": "su",
"": "cai",
"": "mo",
"": "ren",
"": "biao",
"": "qing",
"": "yi",
"": "ji",
"": "wei",
"": "tong",
"": "yi",
"": "ji",
"": "chu",
}
def format_bytes(size):
units = ("B", "KB", "MB", "GB")
value = float(size)
unit_index = 0
while value >= 1024 and unit_index < len(units) - 1:
value /= 1024
unit_index += 1
if unit_index == 0:
return "%d %s" % (size, units[unit_index])
return "%.2f %s (%d B)" % (value, units[unit_index], size)
def image_data_size(image):
return image.width * image.height * len(image.getbands())
def product_size_text(product):
if isinstance(product, Path):
return format_bytes(product.stat().st_size) if product.exists() else "0 B"
if isinstance(product, Image.Image):
return "%dx%d, %s" % (product.width, product.height, format_bytes(image_data_size(product)))
if isinstance(product, list):
item_size = 2 if all(isinstance(item, int) for item in product) else 4
return "%d items, %s" % (len(product), format_bytes(len(product) * item_size))
return str(product)
def print_product(label, product_type, product):
print("%s: type=%s, size=%s" % (label, product_type, product_size_text(product)))
TARGET_CONFIG = {
"eye": {
"header": "eye_image.h",
"source": "eye_image.c",
"include": "eye_image.h",
"array_prefix": "eye_image_image",
"source_prefix": "eye_image_source",
"source_type": "eye_image_source_t",
"source_count_macro": "EYE_IMAGE_SOURCE_COUNT",
"table": "eye_image_sources",
},
"icon": {
"header": "icon_image.h",
"source": "icon_image.c",
"include": "icon_image.h",
"array_prefix": "icon_image",
"source_prefix": "icon_image_source",
"source_type": "icon_image_source_t",
"source_count_macro": "ICON_IMAGE_SOURCE_COUNT",
"table": "icon_image_sources",
},
}
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("input", nargs="?", default=str(DEFAULT_INPUT))
parser.add_argument("--target", choices=("eye", "icon"), default="eye")
parser.add_argument("--out-dir", default=str(DEFAULT_OUTPUT_DIR))
parser.add_argument("--width", type=int, default=DEFAULT_WIDTH)
parser.add_argument("--height", type=int, default=DEFAULT_HEIGHT)
parser.add_argument("--max-frames", type=int, default=0)
parser.add_argument("--step", type=int, default=1)
parser.add_argument("--include-frames", default=DEFAULT_INCLUDE_FRAMES)
parser.add_argument("--rle-items-per-line", type=int, default=DEFAULT_RLE_ITEMS_PER_LINE)
parser.add_argument("--frame-out-dir", default=str(DEFAULT_FRAME_OUTPUT_DIR))
parser.add_argument("--frame-format", default=DEFAULT_FRAME_FORMAT, choices=("png", "bmp", "jpg", "jpeg"))
parser.add_argument("--frame-prefix", default="")
parser.add_argument("--no-save-frames", action="store_true")
parser.add_argument("--clear-frames", action="store_true")
parser.add_argument("--overwrite-image", action="store_true")
return parser.parse_args()
def parse_include_frames(text):
if text.strip() == "":
return None
frames = set()
for item in text.split(","):
item = item.strip()
if item:
frames.add(int(item))
return frames
def is_cjk(char):
return "\u4e00" <= char <= "\u9fff"
def image_name_to_symbol_prefix(name):
parts = []
last_was_separator = False
for char in name:
if char.isascii() and (char.isalnum() or char == "_"):
parts.append(char)
last_was_separator = False
elif char in PINYIN_MAP:
parts.append(PINYIN_MAP[char])
last_was_separator = False
elif is_cjk(char):
parts.append("u%04x" % ord(char))
last_was_separator = False
else:
if parts and not last_was_separator:
parts.append("_")
last_was_separator = True
symbol = "".join(parts).strip("_")
if symbol == "":
symbol = "image"
if symbol[0].isdigit():
symbol = "image_" + symbol
return symbol
def fit_image(image, width, height):
frame = image.convert("RGBA")
frame.thumbnail((width, height), Image.Resampling.LANCZOS)
canvas = Image.new("RGBA", (width, height), (0, 0, 0, 255))
x = (width - frame.width) // 2
y = (height - frame.height) // 2
canvas.alpha_composite(frame, (x, y))
return canvas.convert("RGB")
def rgb_to_rgb565(red, green, blue):
return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3)
def frame_to_rgb565(frame):
return [rgb_to_rgb565(red, green, blue) for red, green, blue in frame.getdata()]
def pixels_to_rle(pixels):
runs = []
for color, group in groupby(pixels):
count = sum(1 for _ in group)
while count > 65535:
runs.append((color, 65535))
count -= 65535
runs.append((color, count))
return runs
def frame_to_rgb888(frame):
return [(red << 16) | (green << 8) | blue for red, green, blue in frame.getdata()]
def format_array_items(items, items_per_line):
lines = []
for index in range(0, len(items), items_per_line):
lines.append(" " + ", ".join(items[index:index + items_per_line]))
return "\n".join(lines)
def rgb888_product_text(label, frame, items_per_line=40):
values = frame_to_rgb888(frame)
items = ["0x%06X" % value for value in values]
return "%s[%d] = {\n%s\n}" % (label, len(values), format_array_items(items, items_per_line))
def rgb565_product_text(label, pixels, items_per_line=40):
items = ["0x%04X" % value for value in pixels]
return "%s[%d] = {\n%s\n}" % (label, len(pixels), format_array_items(items, items_per_line))
def rle_product_text(label, runs, items_per_line=24):
items = ["{0x%04X, %u}" % (color, count) for color, count in runs]
return "%s[%d] = {\n%s\n}" % (label, len(runs), format_array_items(items, items_per_line))
def print_and_collect_product(text, products):
print(text)
products.append(text)
def print_summary_size(label, size):
print("%s: %s" % (label, format_bytes(size)))
def summary_size_text(label, size):
return "%s: %s" % (label, format_bytes(size))
def print_and_collect_line(text, lines):
print(text)
lines.append(text)
def summarize_frames(frames):
frame_count = len(frames)
rgb888_size = sum(image_data_size(frame) for _, frame, _ in frames)
rgb565_size = sum(len(pixels) * 2 for _, _, pixels in frames)
return frame_count, rgb888_size, rgb565_size
def summarize_preview_files(output_dir, frames, frame_format, prefix):
extension = "jpg" if frame_format == "jpeg" else frame_format
total_size = 0
for source_index, _, _ in frames:
path = output_dir / f"{prefix}{source_index:04d}.{extension}"
if path.exists():
total_size += path.stat().st_size
return total_size
def collect_frames(input_path, width, height, max_frames, step, include_frames):
source = Image.open(input_path)
frames = []
for index, raw_frame in enumerate(ImageSequence.Iterator(source)):
if index % step != 0:
continue
if include_frames is not None and index not in include_frames:
continue
frame = fit_image(raw_frame, width, height)
pixels = frame_to_rgb565(frame)
frames.append((index, frame, pixels))
if max_frames > 0 and len(frames) >= max_frames:
break
if len(frames) == 0:
frame = fit_image(source, width, height)
pixels = frame_to_rgb565(frame)
frames.append((0, frame, pixels))
return frames
def write_eye_header(output_dir, width, height, source_count):
_ = (width, height)
path = output_dir / "eye_image.h"
text = (
"#ifndef EYE_IMAGE_H\n"
"#define EYE_IMAGE_H\n\n"
"#include <stdint.h>\n\n"
f"#define EYE_IMAGE_SOURCE_COUNT {source_count}\n\n"
"typedef struct {\n"
" uint16_t color;\n"
" uint16_t count;\n"
"} rle_structure_t;\n\n"
"typedef struct {\n"
" const rle_structure_t *runs;\n"
" uint32_t run_count;\n"
"} eye_image_frame_t;\n\n"
"typedef struct {\n"
" const eye_image_frame_t *frames;\n"
" uint16_t frame_count;\n"
"} eye_image_source_t;\n\n"
"extern const eye_image_source_t eye_image_sources[EYE_IMAGE_SOURCE_COUNT];\n"
"#endif\n"
)
path.write_text(text, encoding="ascii")
def write_icon_header(output_dir, width, height, source_count):
path = output_dir / "icon_image.h"
text = (
"#ifndef ICON_IMAGE_H\n"
"#define ICON_IMAGE_H\n\n"
"#include <stdint.h>\n\n"
"#include \"eye_image.h\"\n\n"
f"#define ICON_IMAGE_SOURCE_COUNT {source_count}\n\n"
"typedef struct {\n"
" const eye_image_frame_t *frames;\n"
" uint16_t frame_count;\n"
" uint16_t width;\n"
" uint16_t height;\n"
"} icon_image_source_t;\n\n"
"extern const icon_image_source_t icon_image_sources[ICON_IMAGE_SOURCE_COUNT];\n\n"
"#endif\n"
)
path.write_text(text, encoding="ascii")
def write_target_header(output_dir, target, width, height, source_count):
if target == "eye":
write_eye_header(output_dir, width, height, source_count)
else:
write_icon_header(output_dir, width, height, source_count)
def source_table_marker(config):
return f"const {config['source_type']} {config['table']}[{config['source_count_macro']}]"
def split_existing_source(path, config):
if not path.exists():
return f'#include "{config["include"]}"\n\n', []
text = path.read_text(encoding="ascii")
marker = source_table_marker(config)
table_pos = text.find(marker)
if table_pos < 0:
if not text.endswith("\n"):
text += "\n"
return text + "\n", []
body = text[:table_pos].rstrip() + "\n\n"
table_text = text[table_pos:]
entries = re.findall(r"\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*(\d+)(?:\s*,\s*(\d+)\s*,\s*(\d+))?\s*\}", table_text)
entries = [
(name, int(count), int(width) if width else None, int(height) if height else None)
for name, count, width, height in entries
if name != "0"
]
return body, entries
def make_unique_symbol(base_name, used_symbols):
symbol = base_name
suffix = 1
while symbol in used_symbols:
symbol = f"{base_name}_{suffix}"
suffix += 1
used_symbols.add(symbol)
return symbol
def write_rle_array(output, symbol, runs, items_per_line):
output.write("static const rle_structure_t %s[] = {\n" % symbol)
for index in range(0, len(runs), items_per_line):
line_runs = runs[index:index + items_per_line]
items = ["{0x%04X, %u}" % (color, count) for color, count in line_runs]
output.write(" %s,\n" % ", ".join(items))
output.write("};\n\n")
def write_source(output_dir, target, frames, width, height, rle_items_per_line, overwrite_image, image_symbol_prefix):
config = TARGET_CONFIG[target]
path = output_dir / config["source"]
if overwrite_image:
existing_body = f'#include "{config["include"]}"\n\n'
source_entries = []
else:
existing_body, source_entries = split_existing_source(path, config)
used_symbols = set(re.findall(r"static const rle_structure_t\s+([A-Za-z_][A-Za-z0-9_]*)\[\]", existing_body))
used_symbols.update(name for name, _, _, _ in source_entries)
source_entries = list(source_entries)
rle_frames = []
rle_total_size = 0
rle_sizes = {}
for source_index, _, pixels in frames:
runs = pixels_to_rle(pixels)
rle_size = len(runs) * 4
rle_total_size += rle_size
rle_sizes[source_index] = rle_size
rle_frames.append((source_index, runs))
source_name = make_unique_symbol(f"{image_symbol_prefix}_source", used_symbols)
max_frame_index = max(source_index for source_index, _ in rle_frames)
frame_entries = {source_index: None for source_index, _ in rle_frames}
with path.open("w", encoding="ascii") as output:
output.write(existing_body)
for frame_number, (source_index, runs) in enumerate(rle_frames):
symbol = make_unique_symbol(f"{image_symbol_prefix}{frame_number}", used_symbols)
write_rle_array(output, symbol, runs, rle_items_per_line)
frame_entries[source_index] = (symbol, len(runs))
output.write("static const eye_image_frame_t %s[] = {\n" % source_name)
for frame_index in range(max_frame_index + 1):
entry = frame_entries.get(frame_index)
if entry is None:
output.write(" {0, 0},\n")
else:
symbol, run_count = entry
output.write(" {%s, %u},\n" % (symbol, run_count))
output.write("};\n\n")
source_entries.append((source_name, max_frame_index + 1, width, height))
output.write("%s = {\n" % source_table_marker(config))
for name, frame_count, source_width, source_height in source_entries:
if target == "icon":
output.write(" {%s, %u, %u, %u},\n" % (name, frame_count, source_width, source_height))
else:
output.write(" {%s, %u},\n" % (name, frame_count))
output.write("};\n")
return len(source_entries), rle_total_size, rle_sizes
def clear_old_frame_sources(output_dir):
for path in output_dir.glob("eye_image_*.c"):
if path.is_file():
path.unlink()
def clear_old_saved_frames(output_dir, prefix, extension):
for path in output_dir.glob(f"{prefix}[0-9][0-9][0-9][0-9].{extension}"):
if path.is_file():
path.unlink()
def save_preview_frame(frame, output_path, image_format):
if image_format in ("jpg", "jpeg"):
frame = frame.convert("RGB")
else:
frame = frame.convert("RGBA")
frame.save(output_path)
def save_preview_frames(output_dir, frames, image_format, prefix, clear_frames):
output_dir.mkdir(parents=True, exist_ok=True)
extension = "jpg" if image_format == "jpeg" else image_format
if clear_frames:
clear_old_saved_frames(output_dir, prefix, extension)
for source_index, frame, _ in frames:
output_path = output_dir / f"{prefix}{source_index:04d}.{extension}"
save_preview_frame(frame, output_path, image_format)
def main():
args = parse_args()
input_path = Path(args.input)
output_dir = Path(args.out_dir)
frame_output_dir = Path(args.frame_out_dir)
frame_format = "jpg" if args.frame_format == "jpeg" else args.frame_format
frame_prefix = args.frame_prefix if args.frame_prefix else input_path.stem
image_symbol_prefix = image_name_to_symbol_prefix(input_path.stem)
if args.step < 1:
raise ValueError("--step must be greater than 0")
if args.rle_items_per_line < 1:
raise ValueError("--rle-items-per-line must be greater than 0")
if args.max_frames < 0:
raise ValueError("--max-frames must be greater than or equal to 0")
output_dir.mkdir(parents=True, exist_ok=True)
include_frames = parse_include_frames(args.include_frames)
frames = collect_frames(input_path, args.width, args.height, args.max_frames, args.step, include_frames)
config = TARGET_CONFIG[args.target]
existing_source_count = 0
if not args.overwrite_image:
_, existing_sources = split_existing_source(output_dir / config["source"], config)
existing_source_count = len(existing_sources)
total_source_count = existing_source_count + 1
clear_old_frame_sources(output_dir)
write_target_header(output_dir, args.target, args.width, args.height, total_source_count)
total_source_count, rle_total_size, rle_sizes = write_source(output_dir, args.target, frames, args.width, args.height, args.rle_items_per_line, args.overwrite_image, image_symbol_prefix)
write_target_header(output_dir, args.target, args.width, args.height, total_source_count)
if not args.no_save_frames:
save_preview_frames(frame_output_dir, frames, frame_format, frame_prefix, args.clear_frames)
frame_count, rgb888_size, rgb565_size = summarize_frames(frames)
product_texts = []
print_and_collect_line("summary:", product_texts)
print_and_collect_line(summary_size_text("input_file", input_path.stat().st_size), product_texts)
print_and_collect_line("frames: %d" % frame_count, product_texts)
for source_index, frame, pixels in frames:
runs = pixels_to_rle(pixels)
print_and_collect_line(
"frame[%d]: rgb888=%s, rgb565=%s, rle=%s" % (
source_index,
format_bytes(image_data_size(frame)),
format_bytes(len(pixels) * 2),
format_bytes(rle_sizes.get(source_index, 0)),
),
product_texts,
)
print_and_collect_product(rgb888_product_text("frame[%d].rgb888" % source_index, frame), product_texts)
print_and_collect_product(rgb565_product_text("frame[%d].rgb565" % source_index, pixels), product_texts)
print_and_collect_product(rle_product_text("frame[%d].rle" % source_index, runs), product_texts)
print_and_collect_line(summary_size_text("rgb888_total", rgb888_size), product_texts)
print_and_collect_line(summary_size_text("rgb565_total", rgb565_size), product_texts)
print_and_collect_line(summary_size_text("rle_total", rle_total_size), product_texts)
frame_output_dir.mkdir(parents=True, exist_ok=True)
product_output_path = frame_output_dir / ("%s_products.txt" % image_symbol_prefix)
product_output_path.write_text("\n".join(product_texts) + "\n", encoding="ascii")
print("products_file: %s" % product_output_path)
if __name__ == "__main__":
main()