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