forked from miklo/svg2ild
139 lines
5.1 KiB
Python
139 lines
5.1 KiB
Python
# ilda/animation.py
|
|
from pathlib import Path
|
|
from typing import List, Tuple, Optional
|
|
|
|
from .frame import Frame
|
|
|
|
|
|
class Animation:
|
|
"""
|
|
A class representing an ILDA animation consisting of multiple frames (SVG files).
|
|
It stores the default ILDA format (e.g. 0), company_name, and projector,
|
|
which are used when creating sections for each frame.
|
|
"""
|
|
|
|
def __init__(self,
|
|
ilda_format: int = 0,
|
|
company_name: str = "MIKLOBIT",
|
|
projector: int = 0) -> None:
|
|
self.ilda_format: int = ilda_format
|
|
self.company_name: str = company_name
|
|
self.projector: int = projector
|
|
|
|
self.frames: List[Frame] = []
|
|
self.frame_names: List[str] = []
|
|
|
|
def set_ilda_format(self, fmt: int) -> None:
|
|
self.ilda_format = fmt
|
|
|
|
def set_company_name(self, name: str) -> None:
|
|
self.company_name = name
|
|
|
|
def set_projector(self, projector: int) -> None:
|
|
self.projector = projector
|
|
|
|
def create_from_file(self,
|
|
svg_path: str,
|
|
simplify: bool = False,
|
|
tol: float = 1.0,
|
|
clear_existing: bool = True) -> None:
|
|
"""
|
|
Load a single SVG file and add a Frame object to the animation.
|
|
The frame name is the file name (stem) truncated to 8 characters.
|
|
The simplify and tol parameters are passed to Frame.read_svg.
|
|
"""
|
|
p = Path(svg_path)
|
|
if clear_existing:
|
|
self.frames = []
|
|
self.frame_names = []
|
|
|
|
if not p.exists():
|
|
raise FileNotFoundError(f"SVG file not found: {svg_path}")
|
|
|
|
frame = Frame()
|
|
frame.read_svg(str(p), simplify=simplify, tol=tol)
|
|
name8 = p.stem[:8]
|
|
self.frames.append(frame)
|
|
self.frame_names.append(name8)
|
|
|
|
def create_from_folder(self,
|
|
folder_path: str,
|
|
simplify: bool = False,
|
|
tol: float = 1.0,
|
|
recursive: bool = False,
|
|
clear_existing: bool = True) -> None:
|
|
"""
|
|
Load all .svg files from the folder (alphabetically) and create Frame objects.
|
|
If recursive=True, it searches subdirectories.
|
|
"""
|
|
p = Path(folder_path)
|
|
if not p.exists() or not p.is_dir():
|
|
raise NotADirectoryError(f"Folder not found: {folder_path}")
|
|
|
|
if clear_existing:
|
|
self.frames = []
|
|
self.frame_names = []
|
|
|
|
if recursive:
|
|
glob_iter = p.rglob("*.svg")
|
|
else:
|
|
glob_iter = p.glob("*.svg")
|
|
|
|
files = sorted(list(glob_iter), key=lambda x: x.name.lower())
|
|
for fp in files:
|
|
try:
|
|
self.create_from_file(str(fp), simplify=simplify, tol=tol, clear_existing=False)
|
|
except Exception as e:
|
|
print(f"Warning: failed to read SVG {fp}: {e}")
|
|
|
|
def _make_eof_header(self) -> bytes:
|
|
"""
|
|
Returns a 32-byte ILDA header indicating EOF (NumberOfRecords = 0).
|
|
Uses the ilda_format/company_name/projector settings from the instance.
|
|
"""
|
|
b = bytearray(32)
|
|
b[0:4] = b'ILDA'
|
|
b[7] = int(self.ilda_format) & 0xFF
|
|
name_enc = self.company_name.encode('ascii', errors='ignore')[:8]
|
|
b[16:16+len(name_enc)] = name_enc # company name field (optional)
|
|
b[24:26] = (0).to_bytes(2, byteorder='big', signed=False) # NumberOfRecords = 0 -> EOF
|
|
b[30] = int(self.projector) & 0xFF
|
|
return bytes(b)
|
|
|
|
def write_ild(self, out_path: str, overwrite: bool = True) -> None:
|
|
"""
|
|
Save the compiled .ilda file: concatenate all sections obtained from Frame.get_ilda
|
|
and add a single EOF header.
|
|
- Skip frames for which get_ilda() will throw a ValueError (e.g., no points).
|
|
- FrameNumber and TotalFrames are set automatically.
|
|
"""
|
|
if not self.frames:
|
|
raise RuntimeError("No frames to write. Use create_from_* first.")
|
|
|
|
outp = Path(out_path)
|
|
if outp.exists() and not overwrite:
|
|
raise FileExistsError(f"File exists: {out_path}")
|
|
|
|
total_frames = len(self.frames)
|
|
bin_stream = bytearray()
|
|
|
|
for idx, frame in enumerate(self.frames):
|
|
frame_name = self.frame_names[idx] if idx < len(self.frame_names) else f"{idx:08d}"
|
|
try:
|
|
section = frame.get_ilda(format_code=self.ilda_format,
|
|
frame_name=frame_name,
|
|
company_name=self.company_name,
|
|
frame_number=idx,
|
|
total_frames=total_frames,
|
|
projector=self.projector)
|
|
except ValueError as ve:
|
|
# return ValueError if frame empty (--> header of 0 records = EOF)
|
|
print(f"Info: skipping empty frame '{frame_name}': {ve}")
|
|
continue
|
|
bin_stream += section
|
|
|
|
bin_stream += self._make_eof_header()
|
|
|
|
with open(outp, 'wb') as f:
|
|
f.write(bin_stream)
|