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) -> Result<(PathBuf, tempfile::TempDir), Box<dyn std::error::Error>> {
12    let file = File::open(zip_path)
13        .map_err(|e| format!("❌ Failed to open zip file '{}': {}", zip_path.display(), e))?;
14
15    let mut archive =
16        ZipArchive::new(file).map_err(|e| format!("❌ Failed to read zip archive: {}", e))?;
17
18    let temp_dir = tempdir().map_err(|e| format!("❌ Failed to create temp dir: {}", e))?;
19    let temp_path = temp_dir.path().to_path_buf();
20
21    let mut extracted = 0u32;
22    for i in 0..archive.len() {
23        let mut file = archive
24            .by_index(i)
25            .map_err(|e| format!("❌ Failed to access file in zip at index {}: {}", i, e))?;
26
27        let filename = file.name().rsplit('/').next().unwrap_or("");
28        if !filename.ends_with(".png") {
29            continue;
30        }
31
32        let full_out_path = temp_path.join(filename);
33        let mut out_file = File::create(&full_out_path).map_err(|e| {
34            format!(
35                "❌ Failed to create output file '{}': {}",
36                full_out_path.display(),
37                e
38            )
39        })?;
40
41        std::io::copy(&mut file, &mut out_file).map_err(|e| {
42            format!(
43                "❌ Failed to copy content to '{}': {}",
44                full_out_path.display(),
45                e
46            )
47        })?;
48
49        println!("✅ Extracting: {}", full_out_path.display());
50        extracted += 1;
51    }
52
53    if extracted == 0 {
54        return Err("❌ No PNG files found in zip archive".into());
55    }
56
57    println!("🗂️  Extracted frames to: {}", temp_path.display());
58    Ok((temp_path.clone(), temp_dir))
59}
60
61/// Open the rendered output in the default system viewer
62pub fn open_output(path: &str) -> std::io::Result<()> {
63    #[cfg(target_os = "macos")]
64    {
65        Command::new("open").arg(path).status().map(|_| ())
66    }
67    #[cfg(target_os = "linux")]
68    {
69        Command::new("xdg-open").arg(path).status().map(|_| ())
70    }
71    #[cfg(target_os = "windows")]
72    {
73        Command::new("cmd")
74            .args(["/C", "start", path])
75            .status()
76            .map(|_| ())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::unzip_frames;
83    use std::fs::File;
84    use std::io::Write;
85    use std::path::Path;
86    use tempfile::tempdir;
87    use zip::write::{FileOptions, ZipWriter};
88    use zip::CompressionMethod;
89
90    // Helper to create a small zip containing two fake PNG files
91    fn create_test_zip(path: &Path) -> zip::result::ZipResult<()> {
92        let file = File::create(path)?;
93        let mut zip = ZipWriter::new(file);
94        let options = FileOptions::default().compression_method(CompressionMethod::Stored);
95
96        zip.start_file("frame_0000.png", options)?;
97        zip.write_all(b"png0")?;
98        zip.start_file("frame_0001.png", options)?;
99        zip.write_all(b"png1")?;
100        zip.finish()?;
101        Ok(())
102    }
103
104    #[test]
105    fn unzip_frames_extracts_pngs() -> Result<(), Box<dyn std::error::Error>> {
106        let dir = tempdir()?;
107        let zip_path = dir.path().join("frames.zip");
108        create_test_zip(&zip_path)?;
109
110        let (out_dir, _guard) = unzip_frames(&zip_path)?;
111
112        let count = std::fs::read_dir(&out_dir)?.count();
113        assert_eq!(count, 2);
114        assert!(out_dir.join("frame_0000.png").exists());
115        assert!(out_dir.join("frame_0001.png").exists());
116
117        Ok(())
118    }
119}