use std::fmt;
use anyhow::{Context, Result, bail};
use bytes::BytesMut;
use crate::frame::{PixelFormat, VideoFrame};
mod brightness;
mod contrast;
mod crop;
mod denoise;
mod grayscale;
mod hflip;
mod invert;
mod overlay;
mod pad;
mod rotate;
mod saturation;
mod vflip;
#[cfg(test)]
mod tests;
pub use denoise::DenoiseMethod;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum VideoFilter {
Crop {
w: u32,
h: u32,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
x: Option<u32>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
y: Option<u32>,
},
Pad {
w: u32,
h: u32,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
x: Option<u32>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
y: Option<u32>,
},
#[cfg_attr(feature = "serde", serde(rename = "hflip"))]
HFlip,
#[cfg_attr(feature = "serde", serde(rename = "vflip"))]
VFlip,
Rotate(u32),
Grayscale,
Overlay {
image: String,
#[cfg_attr(feature = "serde", serde(default))]
x: u32,
#[cfg_attr(feature = "serde", serde(default))]
y: u32,
},
Invert,
Brightness(i32),
Contrast(f32),
Saturation(f32),
Denoise {
#[cfg_attr(feature = "serde", serde(default))]
method: DenoiseMethod,
#[cfg_attr(feature = "serde", serde(default = "denoise::default_denoise_strength"))]
strength: f32,
},
}
impl fmt::Display for VideoFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VideoFilter::Crop { w, h, x: Some(x), y: Some(y) } => write!(f, "crop={w}:{h}:{x}:{y}"),
VideoFilter::Crop { w, h, .. } => write!(f, "crop={w}:{h}"),
VideoFilter::Pad { w, h, x: Some(x), y: Some(y) } => write!(f, "pad={w}:{h}:{x}:{y}"),
VideoFilter::Pad { w, h, .. } => write!(f, "pad={w}:{h}"),
VideoFilter::HFlip => write!(f, "hflip"),
VideoFilter::VFlip => write!(f, "vflip"),
VideoFilter::Rotate(d) => write!(f, "rotate={d}"),
VideoFilter::Grayscale => write!(f, "grayscale"),
VideoFilter::Overlay { image, x, y } => write!(f, "overlay={image}:{x}:{y}"),
VideoFilter::Invert => write!(f, "invert"),
VideoFilter::Brightness(b) => write!(f, "brightness={b}"),
VideoFilter::Contrast(c) => write!(f, "contrast={c}"),
VideoFilter::Saturation(s) => write!(f, "saturation={s}"),
VideoFilter::Denoise { method, strength } => write!(f, "denoise={method}:{strength}"),
}
}
}
pub fn chain_to_string(chain: &[VideoFilter]) -> String {
chain.iter().map(|f| f.to_string()).collect::<Vec<_>>().join(",")
}
#[cfg(feature = "serde")]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[serde(untagged)]
pub enum FilterSpec {
Chain(String),
List(Vec<VideoFilter>),
}
#[cfg(feature = "serde")]
impl FilterSpec {
pub fn resolve(&self) -> Result<Vec<VideoFilter>> {
match self {
FilterSpec::Chain(s) => parse_chain(s),
FilterSpec::List(v) => parse_chain(&chain_to_string(v)),
}
}
pub fn to_chain(&self) -> String {
match self {
FilterSpec::Chain(s) => s.clone(),
FilterSpec::List(v) => chain_to_string(v),
}
}
}
pub fn parse_chain(s: &str) -> Result<Vec<VideoFilter>> {
let mut out = Vec::new();
for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
out.push(parse_one(part)?);
}
if out.is_empty() {
bail!("empty filter chain");
}
Ok(out)
}
fn parse_one(spec: &str) -> Result<VideoFilter> {
let (name, args) = match spec.split_once('=') {
Some((n, a)) => (n.trim(), a.trim()),
None => (spec.trim(), ""),
};
let parts: Vec<&str> = args.split(':').map(str::trim).filter(|s| !s.is_empty()).collect();
let nums = || -> Result<Vec<u32>> {
parts
.iter()
.map(|s| s.parse::<u32>().map_err(|_| anyhow::anyhow!("bad number '{s}' in '{spec}'")))
.collect()
};
let one_f32 = || -> Result<f32> {
parts
.first()
.ok_or_else(|| anyhow::anyhow!("'{name}' needs a value"))?
.parse::<f32>()
.map_err(|_| anyhow::anyhow!("bad number in '{spec}'"))
};
let f = match name {
"crop" => match nums()?.as_slice() {
[w, h] => VideoFilter::Crop { w: *w, h: *h, x: None, y: None },
[w, h, x, y] => VideoFilter::Crop { w: *w, h: *h, x: Some(*x), y: Some(*y) },
_ => bail!("crop wants W:H or W:H:X:Y, got '{args}'"),
},
"pad" => match nums()?.as_slice() {
[w, h] => VideoFilter::Pad { w: *w, h: *h, x: None, y: None },
[w, h, x, y] => VideoFilter::Pad { w: *w, h: *h, x: Some(*x), y: Some(*y) },
_ => bail!("pad wants W:H or W:H:X:Y, got '{args}'"),
},
"hflip" => VideoFilter::HFlip,
"vflip" => VideoFilter::VFlip,
"rotate" | "transpose" => {
let deg = if name == "transpose" { 90 } else { *nums()?.first().unwrap_or(&90) };
if !matches!(deg, 90 | 180 | 270) {
bail!("rotate wants 90|180|270, got {deg}");
}
VideoFilter::Rotate(deg)
}
"grayscale" | "gray" => VideoFilter::Grayscale,
"overlay" => {
let image =
parts.first().ok_or_else(|| anyhow::anyhow!("overlay needs a PATH"))?.to_string();
let x = parts.get(1).map(|s| s.parse::<u32>()).transpose().map_err(|_| anyhow::anyhow!("bad overlay x in '{spec}'"))?.unwrap_or(0);
let y = parts.get(2).map(|s| s.parse::<u32>()).transpose().map_err(|_| anyhow::anyhow!("bad overlay y in '{spec}'"))?.unwrap_or(0);
VideoFilter::Overlay { image, x, y }
}
"invert" | "negate" => VideoFilter::Invert,
"brightness" => {
let b: i32 = parts.first().ok_or_else(|| anyhow::anyhow!("brightness needs a value"))?.parse().map_err(|_| anyhow::anyhow!("bad brightness in '{spec}'"))?;
VideoFilter::Brightness(b)
}
"contrast" => VideoFilter::Contrast(one_f32()?),
"saturation" => VideoFilter::Saturation(one_f32()?),
"denoise" | "nr" => {
let mut method = DenoiseMethod::Bilateral;
let mut strength = 0.5f32;
for &p in &parts {
match p.parse::<f32>() {
Ok(s) => strength = s,
Err(_) => {
method = match p.to_ascii_lowercase().as_str() {
"bilateral" | "bl" => DenoiseMethod::Bilateral,
"gaussian" | "gauss" | "gs" => DenoiseMethod::Gaussian,
"median" | "md" => DenoiseMethod::Median,
"mean" | "box" | "average" => DenoiseMethod::Mean,
"nlmeans" | "nlm" => DenoiseMethod::Nlmeans,
"anisotropic" | "diffusion" | "pm" => DenoiseMethod::Anisotropic,
o => bail!(
"unknown denoise method '{o}' (want bilateral|gaussian|median|\
mean|nlmeans|anisotropic)"
),
};
}
}
}
if !(0.0..=1.0).contains(&strength) {
bail!("denoise strength must be 0.0..=1.0, got {strength}");
}
VideoFilter::Denoise { method, strength }
}
o => bail!("unknown filter '{o}'"),
};
Ok(f)
}
pub fn apply_chain(frame: VideoFrame, chain: &[VideoFilter]) -> Result<VideoFrame> {
let mut f = frame;
for filter in chain {
f = apply(&f, filter)?;
}
Ok(f)
}
pub fn apply(frame: &VideoFrame, filter: &VideoFilter) -> Result<VideoFrame> {
match filter {
VideoFilter::Crop { w, h, x, y } => crop::apply(frame, *w, *h, *x, *y),
VideoFilter::Pad { w, h, x, y } => pad::apply(frame, *w, *h, *x, *y),
VideoFilter::HFlip => hflip::apply(frame),
VideoFilter::VFlip => vflip::apply(frame),
VideoFilter::Rotate(deg) => rotate::apply(frame, *deg),
VideoFilter::Grayscale => grayscale::apply(frame),
VideoFilter::Invert => invert::apply(frame),
VideoFilter::Brightness(delta) => brightness::apply(frame, *delta),
VideoFilter::Contrast(c) => contrast::apply(frame, *c),
VideoFilter::Saturation(s) => saturation::apply(frame, *s),
VideoFilter::Denoise { method, strength } => denoise::apply(frame, *method, *strength),
VideoFilter::Overlay { .. } => {
bail!("overlay is a resource filter — build a FilterChain::prepare(..) and call .apply()")
}
}
}
fn bps(format: PixelFormat) -> Result<usize> {
match format {
PixelFormat::Yuv420p => Ok(1),
PixelFormat::Yuv420p10le => Ok(2),
other => bail!("video filters need Yuv420p / Yuv420p10le, got {other:?}"),
}
}
fn planes(frame: &VideoFrame, bps: usize) -> Result<(&[u8], &[u8], &[u8])> {
let w = frame.width as usize;
let h = frame.height as usize;
let y_len = w * h * bps;
let c_len = (w / 2) * (h / 2) * bps;
if frame.data.len() < y_len + 2 * c_len {
bail!("frame data too small: {} < {} for {}x{}", frame.data.len(), y_len + 2 * c_len, w, h);
}
let (y, rest) = frame.data.split_at(y_len);
let (u, v) = rest.split_at(c_len);
Ok((y, &u[..c_len], &v[..c_len]))
}
fn assemble(src: &VideoFrame, w: u32, h: u32, y: Vec<u8>, u: Vec<u8>, v: Vec<u8>) -> VideoFrame {
let mut data = BytesMut::with_capacity(y.len() + u.len() + v.len());
data.extend_from_slice(&y);
data.extend_from_slice(&u);
data.extend_from_slice(&v);
VideoFrame::new(data.freeze(), w, h, src.format, src.color_space, src.pts)
}
fn planes_8bit(frame: &VideoFrame, what: &str) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
if frame.format != PixelFormat::Yuv420p {
bail!("the `{what}` filter needs an 8-bit Yuv420p frame (got {:?}); it applies to SDR output", frame.format);
}
let (y, u, v) = planes(frame, 1)?;
Ok((y.to_vec(), u.to_vec(), v.to_vec()))
}
fn even(n: u32) -> u32 {
n & !1
}
enum Step {
Plain(VideoFilter),
Overlay(overlay::PreparedOverlay),
}
pub struct FilterChain {
steps: Vec<Step>,
}
impl FilterChain {
pub fn prepare(filters: &[VideoFilter]) -> Result<Self> {
let mut steps = Vec::with_capacity(filters.len());
for f in filters {
match f {
VideoFilter::Overlay { image, x, y } => {
let img = image::ImageReader::open(image)
.with_context(|| format!("opening overlay image '{image}'"))?
.decode()
.with_context(|| format!("decoding overlay image '{image}'"))?
.to_rgba8();
let (w, h) = (img.width(), img.height());
steps.push(Step::Overlay(overlay::PreparedOverlay::from_rgba(img.as_raw(), w, h, *x, *y)?));
}
other => steps.push(Step::Plain(other.clone())),
}
}
Ok(Self { steps })
}
pub fn apply(&self, frame: VideoFrame) -> Result<VideoFrame> {
let mut f = frame;
for step in &self.steps {
f = match step {
Step::Plain(filt) => apply(&f, filt)?,
Step::Overlay(ov) => ov.composite(&f)?,
};
}
Ok(f)
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
}