Files
EmbeddedAnimPacker/EmbeddedAnimPacker.py

175 lines
4.8 KiB
Python

from __future__ import annotations
import argparse
import subprocess
import sys
import tempfile
from pathlib import Path
DEFAULT_LVGL_DIR = Path("lvgl")
DEFAULT_OUTPUT_DIR = Path("littlefs/anim")
DEFAULT_COLOR_FORMAT = "RGB565"
DEFAULT_COMPRESS = "RLE"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Extract frames from a GIF and convert them to LVGL BIN images."
)
parser.add_argument("gif", type=Path, help="Input GIF file.")
parser.add_argument(
"--lvgl-dir",
type=Path,
default=DEFAULT_LVGL_DIR,
help=f"LVGL directory. Default: {DEFAULT_LVGL_DIR}",
)
parser.add_argument(
"-o",
"--output-dir",
type=Path,
default=DEFAULT_OUTPUT_DIR,
help=f"Output directory for generated .bin files. Default: {DEFAULT_OUTPUT_DIR}",
)
parser.add_argument(
"--cf",
"--color-format",
dest="color_format",
default=DEFAULT_COLOR_FORMAT,
help=f"LVGL color format. Default: {DEFAULT_COLOR_FORMAT}",
)
parser.add_argument(
"--compress",
default=DEFAULT_COMPRESS,
choices=("RLE", "LZ4", "NONE"),
help=f"LVGL compression mode. Default: {DEFAULT_COMPRESS}",
)
parser.add_argument(
"--prefix",
default=None,
help="Frame filename prefix. Default: input GIF stem.",
)
parser.add_argument(
"--keep-frames",
type=Path,
default=None,
help="Keep extracted PNG frames in this directory instead of using a temp directory.",
)
parser.add_argument(
"--python",
default=sys.executable,
help="Python executable used to run LVGLImage.py. Default: current interpreter.",
)
return parser.parse_args()
def require_valid_input(gif_path: Path, lvgl_dir: Path) -> Path:
if not gif_path.is_file():
raise FileNotFoundError(f"GIF file not found: {gif_path}")
if gif_path.suffix.lower() != ".gif":
raise ValueError(f"Input must be a .gif file: {gif_path}")
converter = lvgl_dir / "scripts" / "LVGLImage.py"
if not converter.is_file():
raise FileNotFoundError(f"LVGL image converter not found: {converter}")
return converter
def extract_gif_frames(gif_path: Path, output_dir: Path, prefix: str) -> list[Path]:
try:
from PIL import Image, ImageSequence
except ImportError as exc:
raise RuntimeError(
"Pillow is required to read GIF files. Install it with: "
"python3 -m pip install Pillow"
) from exc
output_dir.mkdir(parents=True, exist_ok=True)
frame_paths: list[Path] = []
with Image.open(gif_path) as gif:
for index, frame in enumerate(ImageSequence.Iterator(gif)):
frame_path = output_dir / f"{prefix}_{index:04d}.png"
frame.convert("RGBA").save(frame_path)
frame_paths.append(frame_path)
if not frame_paths:
raise RuntimeError(f"No frames found in GIF: {gif_path}")
return frame_paths
def convert_frame(
frame_path: Path,
converter: Path,
output_dir: Path,
color_format: str,
compress: str,
python_executable: str,
) -> None:
print("Converting:", frame_path)
subprocess.run(
[
python_executable,
str(converter),
"--ofmt",
"BIN",
"--cf",
color_format,
"--compress",
compress,
"-o",
str(output_dir),
str(frame_path),
],
check=True,
)
def main() -> int:
args = parse_args()
prefix = args.prefix or args.gif.stem
converter = require_valid_input(args.gif, args.lvgl_dir)
args.output_dir.mkdir(parents=True, exist_ok=True)
if args.keep_frames:
frame_dir = args.keep_frames
frame_paths = extract_gif_frames(args.gif, frame_dir, prefix)
for frame_path in frame_paths:
convert_frame(
frame_path,
converter,
args.output_dir,
args.color_format,
args.compress,
args.python,
)
else:
with tempfile.TemporaryDirectory(prefix="embedded_anim_") as temp_dir:
frame_paths = extract_gif_frames(args.gif, Path(temp_dir), prefix)
for frame_path in frame_paths:
convert_frame(
frame_path,
converter,
args.output_dir,
args.color_format,
args.compress,
args.python,
)
print(f"Done. Converted {len(frame_paths)} frame(s) to {args.output_dir}.")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
raise SystemExit(1) from exc