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 \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 \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()