574 lines
19 KiB
Python
574 lines
19 KiB
Python
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()
|
|
|
|
|
|
|
|
|
|
|
|
|