initial commit

This commit is contained in:
cottongin
2026-02-05 00:15:54 -05:00
commit 016c9d9e33
16 changed files with 7329 additions and 0 deletions

178
src/encoder/avif.rs Normal file
View File

@@ -0,0 +1,178 @@
use crate::app::EncodingSettings;
use crate::decoder::DecodedFrames;
use libavif::{AvifImage, Encoder, RgbPixels};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum EncodeError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("AVIF encoding error: {0}")]
Avif(String),
}
/// Progress callback type - receives (current, total)
pub type EncodeProgressCallback = Arc<dyn Fn(usize, usize) + Send + Sync>;
pub struct AvifEncoder {
quality: u8,
alpha_quality: u8,
speed: u8,
lossless: bool,
}
impl AvifEncoder {
pub fn new(settings: &EncodingSettings) -> Self {
Self {
quality: if settings.lossless {
100
} else {
settings.quality
},
alpha_quality: if settings.lossless {
100
} else {
settings.alpha_quality
},
speed: settings.speed,
lossless: settings.lossless,
}
}
pub fn encode(&self, frames: &DecodedFrames, output_path: &Path) -> Result<(), EncodeError> {
self.encode_with_progress(frames, output_path, None)
}
pub fn encode_with_progress(
&self,
frames: &DecodedFrames,
output_path: &Path,
progress: Option<EncodeProgressCallback>,
) -> Result<(), EncodeError> {
if frames.frames.is_empty() {
return Err(EncodeError::Avif("No frames to encode".to_string()));
}
let data = if frames.is_animated() {
self.encode_animated_with_progress(frames, progress)?
} else {
// Report progress for single frame
if let Some(ref cb) = progress {
cb(0, 1);
}
let result = self.encode_single(frames)?;
if let Some(ref cb) = progress {
cb(1, 1);
}
result
};
let mut file = File::create(output_path)?;
file.write_all(&data)?;
Ok(())
}
fn encode_single(&self, frames: &DecodedFrames) -> Result<Vec<u8>, EncodeError> {
let frame = &frames.frames[0];
// Create AvifImage from RGBA data
let image = self.create_avif_image(
frame.width,
frame.height,
&frame.rgba,
frames.has_alpha,
)?;
// Configure encoder
let mut encoder = Encoder::new();
self.configure_encoder(&mut encoder);
// Encode single image
let data = encoder
.encode(&image)
.map_err(|e| EncodeError::Avif(format!("Encoding failed: {:?}", e)))?;
Ok(data.to_vec())
}
fn encode_animated(&self, frames: &DecodedFrames) -> Result<Vec<u8>, EncodeError> {
self.encode_animated_with_progress(frames, None)
}
fn encode_animated_with_progress(
&self,
frames: &DecodedFrames,
progress: Option<EncodeProgressCallback>,
) -> Result<Vec<u8>, EncodeError> {
let mut encoder = Encoder::new();
self.configure_encoder(&mut encoder);
// Set timescale to milliseconds
encoder.set_timescale(1000);
let total_frames = frames.frames.len();
for (i, frame) in frames.frames.iter().enumerate() {
let image = self.create_avif_image(
frame.width,
frame.height,
&frame.rgba,
frames.has_alpha,
)?;
// Add frame with duration
encoder
.add_image(&image, frame.duration_ms, libavif::AddImageFlags::NONE)
.map_err(|e| EncodeError::Avif(format!("Failed to add frame: {:?}", e)))?;
// Report progress after each frame is added
if let Some(ref cb) = progress {
cb(i + 1, total_frames);
}
}
let data = encoder
.finish()
.map_err(|e| EncodeError::Avif(format!("Failed to finish encoding: {:?}", e)))?;
Ok(data.to_vec())
}
fn create_avif_image(
&self,
width: u32,
height: u32,
rgba: &[u8],
has_alpha: bool,
) -> Result<AvifImage, EncodeError> {
// Create RGB pixels from RGBA data
let rgb = RgbPixels::new(width, height, rgba)
.map_err(|e| EncodeError::Avif(format!("Failed to create RGB pixels: {:?}", e)))?;
// Convert to AvifImage
let image = rgb.to_image(if has_alpha {
libavif::YuvFormat::Yuv444
} else {
libavif::YuvFormat::Yuv420
});
Ok(image)
}
fn configure_encoder(&self, encoder: &mut Encoder) {
// Quality 0-100 maps directly to libavif quality
encoder.set_quality(self.quality);
encoder.set_alpha_quality(self.alpha_quality);
encoder.set_speed(self.speed);
if self.lossless {
encoder.set_quality(100);
encoder.set_alpha_quality(100);
}
}
}