use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use tempfile::tempdir;
use zip::ZipArchive;
fn is_valid_image(file_name: &str) -> bool {
let name = file_name.to_lowercase();
name.ends_with(".png") && !name.starts_with("._")
}
pub fn unzip_frames(
zip_path: &Path,
verbose: bool,
) -> Result<(PathBuf, tempfile::TempDir), Box<dyn std::error::Error>> {
let file = File::open(zip_path)
.map_err(|e| format!("❌ Failed to open zip file '{}': {}", zip_path.display(), e))?;
let mut archive =
ZipArchive::new(file).map_err(|e| format!("❌ Failed to read zip archive: {}", e))?;
let temp_dir = tempdir().map_err(|e| format!("❌ Failed to create temp dir: {}", e))?;
let temp_path = temp_dir.path().to_path_buf();
let mut extracted = 0u32;
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| format!("❌ Failed to access file in zip at index {}: {}", i, e))?;
let filename = file.name().rsplit('/').next().unwrap_or("");
if !is_valid_image(filename) {
continue;
}
let full_out_path = temp_path.join(filename);
let mut out_file = File::create(&full_out_path).map_err(|e| {
format!(
"❌ Failed to create output file '{}': {}",
full_out_path.display(),
e
)
})?;
std::io::copy(&mut file, &mut out_file).map_err(|e| {
format!(
"❌ Failed to copy content to '{}': {}",
full_out_path.display(),
e
)
})?;
if verbose {
println!("✅ Extracting: {}", full_out_path.display());
}
extracted += 1;
}
if extracted == 0 {
return Err("❌ No PNG files found in zip archive".into());
}
if verbose {
if extracted > 1 {
println!("⚠️ Extracted {} frames from zip", extracted);
} else {
println!("✅ Extracted 1 frame from zip");
}
println!("🗂️ Extracted frames to: {}", temp_path.display());
}
Ok((temp_path.clone(), temp_dir))
}
pub fn count_pngs_in_zip(zip_path: &Path) -> Result<usize, Box<dyn std::error::Error>> {
let file = File::open(zip_path)
.map_err(|e| format!("❌ Failed to open zip file '{}': {}", zip_path.display(), e))?;
let mut archive =
ZipArchive::new(file).map_err(|e| format!("❌ Failed to read zip archive: {}", e))?;
let mut count = 0usize;
for i in 0..archive.len() {
let file = archive.by_index(i)?;
let filename = file.name().rsplit('/').next().unwrap_or("");
if is_valid_image(filename) {
count += 1;
}
}
Ok(count)
}
pub fn extract_frame_from_zip(
zip_path: &Path,
frame_index: usize,
output: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(zip_path)
.map_err(|e| format!("❌ Failed to open zip file '{}': {}", zip_path.display(), e))?;
let mut archive =
ZipArchive::new(file).map_err(|e| format!("❌ Failed to read zip archive: {}", e))?;
let mut png_indices = Vec::new();
for i in 0..archive.len() {
let f = archive.by_index(i)?;
let name = f.name().rsplit('/').next().unwrap_or("");
if is_valid_image(name) {
png_indices.push(i);
}
}
if png_indices.is_empty() {
return Err("❌ No PNG files found in zip archive".into());
}
if frame_index >= png_indices.len() {
return Err(format!(
"❌ Frame index {} out of range (0..{})",
frame_index,
png_indices.len() - 1
)
.into());
}
let mut file = archive.by_index(png_indices[frame_index])?;
let mut out = File::create(output).map_err(|e| {
format!(
"❌ Failed to create output file '{}': {}",
output.display(),
e
)
})?;
std::io::copy(&mut file, &mut out)
.map_err(|e| format!("❌ Failed to copy content to '{}': {}", output.display(), e))?;
Ok(())
}
pub fn open_output(path: &str) -> std::io::Result<()> {
#[cfg(target_os = "macos")]
{
Command::new("open").arg(path).status().map(|_| ())
}
#[cfg(target_os = "linux")]
{
Command::new("xdg-open").arg(path).status().map(|_| ())
}
#[cfg(target_os = "windows")]
{
Command::new("cmd")
.args(["/C", "start", path])
.status()
.map(|_| ())
}
}
pub fn scan_ffmpeg_stderr(stderr: &str) -> Vec<String> {
let mut warnings = Vec::new();
let stderr_lc = stderr.to_lowercase();
if stderr_lc.contains("drop") {
warnings.push("⚠️ FFmpeg reported frame drops.".to_string());
}
if stderr_lc.contains("missing") {
warnings.push("⚠️ Possible missing or unreadable frame(s).".to_string());
}
if stderr_lc.contains("buffer") || stderr_lc.contains("underrun") {
warnings.push("⚠️ Buffer underrun or encoding delay detected.".to_string());
}
if stderr_lc.contains("deprecated") {
warnings.push("⚠️ Deprecated options used in FFmpeg command.".to_string());
}
if stderr_lc.contains("high frame rate") {
warnings.push("⚠️ High frame rate detected, may cause performance issues.".to_string());
}
if stderr_lc.contains("invalid frame") {
warnings.push("⚠️ Invalid frame detected in input.".to_string());
}
if stderr_lc.contains("no such file or directory") {
warnings.push("⚠️ Input file not found or inaccessible.".to_string());
}
if stderr_lc.contains("unrecognized option") {
warnings.push("⚠️ Unrecognized FFmpeg option used.".to_string());
}
if stderr_lc.contains("error") {
warnings.push("❌ FFmpeg encountered an error.".to_string());
}
if stderr_lc.contains("warning") {
warnings.push("⚠️ FFmpeg issued a warning.".to_string());
}
if stderr_lc.contains("frame rate very high") {
warnings
.push("⚠️ Frame rate very high for a muxer not efficiently supporting it.".to_string());
}
if stderr.contains("duration") && stderr.contains("Past") {
warnings.push("⚠️ Past frame duration too large.".to_string());
}
warnings
}
pub fn run_ffmpeg_with_output(args: &[String]) -> Result<(ExitStatus, String), String> {
let output = Command::new("ffmpeg").args(args).output().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
"❌ ffmpeg not found in PATH.".to_string()
} else {
format!("❌ ffmpeg failed to run: {}", e)
}
})?;
if !output.status.success() {
return Err(format!(
"❌ ffmpeg exited with code {}",
output.status.code().unwrap_or(-1)
));
}
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok((output.status, stderr))
}
#[cfg(test)]
mod tests {
use super::unzip_frames;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use tempfile::tempdir;
use zip::write::{FileOptions, ZipWriter};
use zip::CompressionMethod;
fn create_test_zip(path: &Path) -> zip::result::ZipResult<()> {
let file = File::create(path)?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
zip.start_file("frame_0000.png", options)?;
zip.write_all(b"png0")?;
zip.start_file("frame_0001.png", options)?;
zip.write_all(b"png1")?;
zip.finish()?;
Ok(())
}
#[test]
fn unzip_frames_extracts_pngs() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempdir()?;
let zip_path = dir.path().join("frames.zip");
create_test_zip(&zip_path)?;
let (out_dir, _guard) = unzip_frames(&zip_path, false)?;
let count = std::fs::read_dir(&out_dir)?.count();
assert_eq!(count, 2);
assert!(out_dir.join("frame_0000.png").exists());
assert!(out_dir.join("frame_0001.png").exists());
Ok(())
}
}