mod gif;
pub use gif::GifEncoder;
use crate::core::{PlottingError, Result};
use std::path::Path;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Quality {
Low,
#[default]
Medium,
High,
Lossless,
}
impl Quality {
#[cfg(feature = "animation-video")]
#[deprecated(note = "AV1 video output is not currently implemented")]
pub fn to_rav1e_speed(self) -> u8 {
match self {
Quality::Low => 10,
Quality::Medium => 6,
Quality::High => 2,
Quality::Lossless => 0,
}
}
pub fn to_gif_speed(self) -> i32 {
match self {
Quality::Low => 30,
Quality::Medium => 10,
Quality::High => 1,
Quality::Lossless => 1,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Codec {
Gif,
Av1,
#[default]
Auto,
}
impl Codec {
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"gif" => Some(Codec::Gif),
"mp4" | "webm" | "mkv" => Some(Codec::Av1),
_ => None,
}
}
pub fn default_extension(&self) -> &'static str {
match self {
Codec::Gif => "gif",
Codec::Av1 => "mp4",
Codec::Auto => "gif",
}
}
}
pub trait Encoder: Send {
fn init(&mut self, width: u32, height: u32) -> Result<()>;
fn encode_frame(&mut self, rgb_data: &[u8], timestamp_ms: u64) -> Result<()>;
fn finalize(self: Box<Self>) -> Result<()>;
fn extensions(&self) -> &[&str];
fn supports_extension(&self, ext: &str) -> bool {
let ext_lower = ext.to_lowercase();
self.extensions()
.iter()
.any(|e| e.to_lowercase() == ext_lower)
}
}
pub fn create_encoder(path: &Path, quality: Quality) -> Result<Box<dyn Encoder>> {
let Some(extension) = path.extension() else {
return Ok(Box::new(GifEncoder::new(path, quality)?));
};
let ext = extension
.to_str()
.ok_or_else(|| PlottingError::UnsupportedFormat("non-UTF-8 extension".into()))?;
match Codec::from_extension(ext) {
Some(Codec::Gif) => Ok(Box::new(GifEncoder::new(path, quality)?)),
Some(Codec::Av1) => unsupported_av1(),
Some(Codec::Auto) => {
Ok(Box::new(GifEncoder::new(path, quality)?))
}
None => Err(PlottingError::UnsupportedFormat(ext.to_string())),
}
}
pub(super) fn create_encoder_for_codec(
path: &Path,
quality: Quality,
codec: Codec,
) -> Result<Box<dyn Encoder>> {
match codec {
Codec::Auto => create_encoder(path, quality),
Codec::Gif => Ok(Box::new(GifEncoder::new(path, quality)?)),
Codec::Av1 => unsupported_av1(),
}
}
fn unsupported_av1<T>() -> Result<T> {
Err(PlottingError::UnsupportedOperation {
operation: "AV1 animation encoding",
reason: "no AV1 container/muxing implementation is currently shipped".into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codec_from_extension() {
assert_eq!(Codec::from_extension("gif"), Some(Codec::Gif));
assert_eq!(Codec::from_extension("GIF"), Some(Codec::Gif));
assert_eq!(Codec::from_extension("mp4"), Some(Codec::Av1));
assert_eq!(Codec::from_extension("webm"), Some(Codec::Av1));
assert_eq!(Codec::from_extension("unknown"), None);
}
#[test]
fn test_quality_gif_speed() {
assert_eq!(Quality::Low.to_gif_speed(), 30);
assert_eq!(Quality::Medium.to_gif_speed(), 10);
assert_eq!(Quality::High.to_gif_speed(), 1);
}
#[test]
fn test_codec_default_extension() {
assert_eq!(Codec::Gif.default_extension(), "gif");
assert_eq!(Codec::Av1.default_extension(), "mp4");
}
#[test]
fn unknown_extensions_are_rejected_instead_of_writing_gif_data() {
let result = create_encoder(Path::new("animation.unknown"), Quality::Medium);
assert!(
matches!(result, Err(PlottingError::UnsupportedFormat(extension)) if extension == "unknown")
);
}
#[test]
fn explicit_gif_codec_does_not_depend_on_the_path_extension() {
assert!(
create_encoder_for_codec(Path::new("animation.custom"), Quality::Medium, Codec::Gif)
.is_ok()
);
}
#[test]
fn av1_is_reported_as_an_unsupported_operation() {
let result =
create_encoder_for_codec(Path::new("animation.webm"), Quality::Medium, Codec::Av1);
assert!(matches!(
result,
Err(PlottingError::UnsupportedOperation {
operation: "AV1 animation encoding",
..
})
));
}
}