aether_renderer_core/
utils.rs

1use std::fs::File;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use tempfile::tempdir;
5use zip::ZipArchive;
6
7/// Extracts `frame_*.png` from a ZIP into a temporary folder and returns the
8/// folder path along with the temp directory guard.
9pub fn unzip_frames(
10    zip_path: &Path,
11    verbose: bool,
12) -> Result<(PathBuf, tempfile::TempDir), Box<dyn std::error::Error>> {
13    let file = File::open(zip_path)
14        .map_err(|e| format!("❌ Failed to open zip file '{}': {}", zip_path.display(), e))?;
15
16    let mut archive =
17        ZipArchive::new(file).map_err(|e| format!("❌ Failed to read zip archive: {}", e))?;
18
19    let temp_dir = tempdir().map_err(|e| format!("❌ Failed to create temp dir: {}", e))?;
20    let temp_path = temp_dir.path().to_path_buf();
21
22    let mut extracted = 0u32;
23    for i in 0..archive.len() {
24        let mut file = archive
25            .by_index(i)
26            .map_err(|e| format!("❌ Failed to access file in zip at index {}: {}", i, e))?;
27
28        let filename = file.name().rsplit('/').next().unwrap_or("");
29        if !filename.ends_with(".png") {
30            continue;
31        }
32
33        let full_out_path = temp_path.join(filename);
34        let mut out_file = File::create(&full_out_path).map_err(|e| {
35            format!(
36                "❌ Failed to create output file '{}': {}",
37                full_out_path.display(),
38                e
39            )
40        })?;
41
42        std::io::copy(&mut file, &mut out_file).map_err(|e| {
43            format!(
44                "❌ Failed to copy content to '{}': {}",
45                full_out_path.display(),
46                e
47            )
48        })?;
49
50        if verbose {
51            println!("✅ Extracting: {}", full_out_path.display());
52        }
53        extracted += 1;
54    }
55
56    if extracted == 0 {
57        return Err("❌ No PNG files found in zip archive".into());
58    }
59
60    if verbose {
61        if extracted > 1 {
62            println!("⚠️  Extracted {} frames from zip", extracted);
63        } else {
64            println!("✅ Extracted 1 frame from zip");
65        }
66        println!("🗂️  Extracted frames to: {}", temp_path.display());
67    }
68    Ok((temp_path.clone(), temp_dir))
69}
70
71/// Count PNG files inside a ZIP archive
72pub fn count_pngs_in_zip(zip_path: &Path) -> Result<usize, Box<dyn std::error::Error>> {
73    let file = File::open(zip_path)
74        .map_err(|e| format!("❌ Failed to open zip file '{}': {}", zip_path.display(), e))?;
75    let mut archive =
76        ZipArchive::new(file).map_err(|e| format!("❌ Failed to read zip archive: {}", e))?;
77    let mut count = 0usize;
78    for i in 0..archive.len() {
79        let file = archive.by_index(i)?;
80        let filename = file.name().rsplit('/').next().unwrap_or("");
81        if filename.ends_with(".png") {
82            count += 1;
83        }
84    }
85    Ok(count)
86}
87
88/// Extract a specific PNG frame from a ZIP archive
89pub fn extract_frame_from_zip(
90    zip_path: &Path,
91    frame_index: usize,
92    output: &Path,
93) -> Result<(), Box<dyn std::error::Error>> {
94    let file = File::open(zip_path)
95        .map_err(|e| format!("❌ Failed to open zip file '{}': {}", zip_path.display(), e))?;
96    let mut archive =
97        ZipArchive::new(file).map_err(|e| format!("❌ Failed to read zip archive: {}", e))?;
98    let mut png_indices = Vec::new();
99    for i in 0..archive.len() {
100        let f = archive.by_index(i)?;
101        let name = f.name().rsplit('/').next().unwrap_or("");
102        if name.ends_with(".png") {
103            png_indices.push(i);
104        }
105    }
106    if png_indices.is_empty() {
107        return Err("❌ No PNG files found in zip archive".into());
108    }
109    if frame_index >= png_indices.len() {
110        return Err(format!(
111            "❌ Frame index {} out of range (0..{})",
112            frame_index,
113            png_indices.len() - 1
114        )
115        .into());
116    }
117    let mut file = archive.by_index(png_indices[frame_index])?;
118    let mut out = File::create(output).map_err(|e| {
119        format!(
120            "❌ Failed to create output file '{}': {}",
121            output.display(),
122            e
123        )
124    })?;
125    std::io::copy(&mut file, &mut out)
126        .map_err(|e| format!("❌ Failed to copy content to '{}': {}", output.display(), e))?;
127    Ok(())
128}
129
130/// Open the rendered output in the default system viewer
131pub fn open_output(path: &str) -> std::io::Result<()> {
132    #[cfg(target_os = "macos")]
133    {
134        Command::new("open").arg(path).status().map(|_| ())
135    }
136    #[cfg(target_os = "linux")]
137    {
138        Command::new("xdg-open").arg(path).status().map(|_| ())
139    }
140    #[cfg(target_os = "windows")]
141    {
142        Command::new("cmd")
143            .args(["/C", "start", path])
144            .status()
145            .map(|_| ())
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::unzip_frames;
152    use std::fs::File;
153    use std::io::Write;
154    use std::path::Path;
155    use tempfile::tempdir;
156    use zip::write::{FileOptions, ZipWriter};
157    use zip::CompressionMethod;
158
159    // Helper to create a small zip containing two fake PNG files
160    fn create_test_zip(path: &Path) -> zip::result::ZipResult<()> {
161        let file = File::create(path)?;
162        let mut zip = ZipWriter::new(file);
163        let options = FileOptions::default().compression_method(CompressionMethod::Stored);
164
165        zip.start_file("frame_0000.png", options)?;
166        zip.write_all(b"png0")?;
167        zip.start_file("frame_0001.png", options)?;
168        zip.write_all(b"png1")?;
169        zip.finish()?;
170        Ok(())
171    }
172
173    #[test]
174    fn unzip_frames_extracts_pngs() -> Result<(), Box<dyn std::error::Error>> {
175        let dir = tempdir()?;
176        let zip_path = dir.path().join("frames.zip");
177        create_test_zip(&zip_path)?;
178
179        let (out_dir, _guard) = unzip_frames(&zip_path, false)?;
180
181        let count = std::fs::read_dir(&out_dir)?.count();
182        assert_eq!(count, 2);
183        assert!(out_dir.join("frame_0000.png").exists());
184        assert!(out_dir.join("frame_0001.png").exists());
185
186        Ok(())
187    }
188}