use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use crate::{ColorSpace, Error};
use ffcine_core::color::named_enum;
use ffcine_core::color::{HdrMetadata, Matrix, Primaries, Range, Transfer};
use ffcine_core::geom::Mat3;
use ffcine_core::lut::{Lut, LutType};
pub use ffcine_filters::{Denoise, Grain, Spatial, Temporal};
named_enum!(
ToneMapping {
Auto = "auto",
Bt2390 = "bt.2390",
Bt2446a = "bt.2446a",
Spline = "spline",
Reinhard = "reinhard",
Mobius = "mobius",
Hable = "hable",
Clip = "clip",
St2094_40 = "st2094-40",
St2094_10 = "st2094-10",
Gamma = "gamma",
Linear = "linear",
}
);
named_enum!(
GamutMapping {
Perceptual = "perceptual",
Relative = "relative",
Saturation = "saturation",
Absolute = "absolute",
Desaturate = "desaturate",
Darken = "darken",
Clip = "clip",
Softclip = "softclip",
Highlight = "highlight",
Linear = "linear",
}
);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ColorMapTuning {
pub contrast_recovery: f32,
pub perceptual_strength: f32,
pub perceptual_deadzone: f32,
}
impl Default for ColorMapTuning {
fn default() -> Self {
Self {
contrast_recovery: 0.30,
perceptual_strength: 0.80,
perceptual_deadzone: 0.30,
}
}
}
named_enum!(
Scaler {
Lanczos = "lanczos",
EwaLanczos = "ewa_lanczos",
EwaLanczosSharp = "ewa_lanczossharp",
Spline36 = "spline36",
Mitchell = "mitchell",
Bicubic = "bicubic",
Bilinear = "bilinear",
Nearest = "nearest",
}
);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Scaling {
pub upscaler: Scaler,
pub downscaler: Scaler,
pub linear: bool,
pub sigmoid: bool,
}
impl Default for Scaling {
fn default() -> Self {
Self {
upscaler: Scaler::Lanczos,
downscaler: Scaler::Lanczos,
linear: false,
sigmoid: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Size {
pub width: u32,
pub height: u32,
}
impl FromStr for Size {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
let invalid = || Error::Invalid {
what: "size (WxH)",
value: s.to_owned(),
};
let (w, h) = s.split_once('x').ok_or_else(invalid)?;
Ok(Self {
width: w.parse().map_err(|_| invalid())?,
height: h.parse().map_err(|_| invalid())?,
})
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}", self.width, self.height)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Crop {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl FromStr for Crop {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
let invalid = || Error::Invalid {
what: "crop (w:h:x:y)",
value: s.to_owned(),
};
let v: Vec<f32> = s
.split(':')
.map(str::parse)
.collect::<Result<_, _>>()
.map_err(|_| invalid())?;
match v[..] {
[width, height, x, y] => Ok(Self {
x,
y,
width,
height,
}),
_ => Err(invalid()),
}
}
}
named_enum!(
DitherMethod {
Blue = "blue",
Ordered = "ordered",
White = "white",
}
);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Dither {
pub method: DitherMethod,
pub temporal: bool,
}
impl Default for Dither {
fn default() -> Self {
Self {
method: DitherMethod::Blue,
temporal: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Deband {
pub iterations: u32,
pub threshold: f32,
pub radius: f32,
pub grain: f32,
}
impl Default for Deband {
fn default() -> Self {
Self {
iterations: 1,
threshold: 4.0,
radius: 16.0,
grain: 6.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ColorOverride {
pub primaries: Option<Primaries>,
pub transfer: Option<Transfer>,
pub matrix: Option<Matrix>,
pub range: Option<Range>,
}
impl FromStr for ColorOverride {
type Err = Error;
#[allow(clippy::many_single_char_names)]
fn from_str(s: &str) -> Result<Self, Error> {
fn opt<T: FromStr<Err = Error>>(v: &str) -> Result<Option<T>, Error> {
if v == "-" {
Ok(None)
} else {
v.parse().map(Some)
}
}
let v: Vec<&str> = s.split('/').collect();
match v[..] {
[m, p, t, r] => Ok(Self {
matrix: opt(m)?,
primaries: opt(p)?,
transfer: opt(t)?,
range: opt(r)?,
}),
_ => Err(Error::Invalid {
what: "colour override (matrix/primaries/transfer/range)",
value: s.to_owned(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LutStage {
pub lut: Lut,
pub kind: LutType,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Warp {
pub transforms: Vec<Mat3>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Hook {
pub name: String,
pub source: String,
pub params: Vec<(String, f64)>,
pub tracks: Vec<ParamTrack>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ParamTrack {
pub name: String,
pub values: Vec<f32>,
}
impl ParamTrack {
pub fn at(&self, frame: usize) -> Option<f32> {
self.values.get(frame).or(self.values.last()).copied()
}
}
impl Grade {
pub fn merge_luts(&mut self) {
let (Some(cam), Some(lut)) = (&self.input_lut, &self.lut) else {
return;
};
if cam.kind != LutType::Normalized || lut.kind != LutType::Normalized {
return;
}
let size = cam.lut.size.max(lut.lut.size).max(33);
let baked = crate::bake::Bake {
camera: Some(&cam.lut),
lut: Some(&lut.lut),
look: crate::shaders::LookParams::default(),
}
.bake(size);
self.input_lut = Some(LutStage {
lut: baked,
kind: LutType::Normalized,
});
self.lut = None;
}
pub fn same_frame(&self, a: usize, b: usize) -> bool {
self.crop_at(a) == self.crop_at(b)
&& self.warp.is_none()
&& self.grain.is_none()
&& self.shaders.is_empty()
&& self.dither.is_none_or(|d| !d.temporal)
&& self.hooks.iter().all(|h| {
!h.source
.contains(&format!("//!PARAM {}", ffcine_stabilize::hook::FRAME_PARAM))
&& h.tracks.iter().all(|t| t.at(a) == t.at(b))
})
}
pub fn crop_at(&self, i: usize) -> Option<Crop> {
self.crops
.get(i)
.or(self.crops.last())
.copied()
.or(self.crop)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Border {
#[default]
Black,
Blur(f32),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Grade {
pub input_color: ColorOverride,
pub input_lut: Option<LutStage>,
pub lut: Option<LutStage>,
pub rgb_gain: Option<[f32; 3]>,
pub tone_mapping: ToneMapping,
pub gamut_mapping: GamutMapping,
pub color_map: ColorMapTuning,
pub peak_detect: bool,
pub deband: Option<Deband>,
pub dither: Option<Dither>,
pub crop: Option<Crop>,
pub warp: Option<Warp>,
pub crops: Vec<Crop>,
pub place: Option<Crop>,
pub border: Border,
pub size: Option<Size>,
pub scaling: Scaling,
pub sharpen: f32,
pub hooks: Vec<Hook>,
pub shaders: Vec<PathBuf>,
pub denoise: Option<Denoise>,
pub grain: Option<Grain>,
pub output: ColorSpace,
pub output_hdr: Option<HdrMetadata>,
}
impl Default for Grade {
fn default() -> Self {
Self {
input_color: ColorOverride::default(),
input_lut: None,
lut: None,
rgb_gain: None,
tone_mapping: ToneMapping::Bt2390,
gamut_mapping: GamutMapping::Perceptual,
color_map: ColorMapTuning::default(),
peak_detect: true,
deband: None,
dither: Some(Dither::default()),
crop: None,
warp: None,
crops: Vec::new(),
place: None,
border: Border::Black,
size: None,
scaling: Scaling::default(),
sharpen: 0.0,
hooks: Vec::new(),
shaders: Vec::new(),
denoise: None,
grain: None,
output: ColorSpace::BT709_TV,
output_hdr: None,
}
}
}
#[cfg(test)]
mod same_frame_tests {
use super::*;
#[test]
fn same_frame_follows_tracks_and_crops() {
let mut g = Grade {
dither: None,
hooks: vec![Hook {
name: "alpha".into(),
source: crate::shaders::layer_alpha().into(),
params: Vec::new(),
tracks: vec![ParamTrack {
name: "opacity".into(),
values: vec![0.0, 0.5, 1.0, 1.0],
}],
}],
..Grade::default()
};
assert!(!g.same_frame(0, 1));
assert!(g.same_frame(2, 3) && g.same_frame(3, 9));
let c = |x| Crop {
x,
y: 0.0,
width: 10.0,
height: 10.0,
};
g.crops = vec![c(0.0), c(0.0), c(0.0), c(1.0)];
assert!(!g.same_frame(2, 3));
g.crops.clear();
g.dither = Some(Dither {
temporal: true,
..Dither::default()
});
assert!(!g.same_frame(2, 3));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merges_camera_and_grade_luts() {
let mut half = Lut::identity(5);
for e in &mut half.data {
*e = e.map(|v| v * 0.5);
}
let stage = |lut| LutStage {
lut,
kind: LutType::Normalized,
};
let mut g = Grade {
input_lut: Some(stage(half.clone())),
lut: Some(stage(half)),
..Grade::default()
};
g.merge_luts();
assert!(g.lut.is_none());
let m = g.input_lut.unwrap().lut;
assert_eq!(m.size, 33);
assert!((m.sample([1.0, 0.5, 0.0])[0] - 0.25).abs() < 1e-5);
}
#[test]
fn parse_size_and_crop() {
assert_eq!(
"3840x2160".parse::<Size>().unwrap(),
Size {
width: 3840,
height: 2160
}
);
assert!("3840".parse::<Size>().is_err());
let c: Crop = "1920:1080:10:20".parse().unwrap();
assert_eq!((c.width, c.x), (1920.0, 10.0));
assert!("1:2:3".parse::<Crop>().is_err());
let o: ColorOverride = "-/bt709/bt709/tv".parse().unwrap();
assert_eq!(o.matrix, None);
assert_eq!(o.range, Some(Range::Limited));
assert!("bt709/bt709".parse::<ColorOverride>().is_err());
assert_eq!(
"bt.2390".parse::<ToneMapping>().unwrap(),
ToneMapping::Bt2390
);
assert_eq!(
"st2094-40".parse::<ToneMapping>().unwrap(),
ToneMapping::St2094_40
);
assert_eq!(
"softclip".parse::<GamutMapping>().unwrap(),
GamutMapping::Softclip
);
}
}