#![forbid(unsafe_code)]
#![allow(dead_code)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::similar_names)]
#![allow(clippy::module_name_repetitions)]
use super::film_grain::{FilmGrainParams, ScalingPoint};
use std::collections::BTreeMap;
#[derive(Clone, Debug)]
pub struct BlockGrainOverride {
pub block_col: u32,
pub block_row: u32,
pub scaling_delta: i8,
pub ar_lag_delta: i8,
pub seed_xor: u16,
}
impl BlockGrainOverride {
#[must_use]
pub fn new(block_col: u32, block_row: u32) -> Self {
Self {
block_col,
block_row,
scaling_delta: 0,
ar_lag_delta: 0,
seed_xor: 0,
}
}
#[must_use]
pub fn apply(&self, base: &FilmGrainParams) -> FilmGrainParams {
let mut p = base.clone();
p.grain_scaling_minus_8 =
(i16::from(p.grain_scaling_minus_8) + i16::from(self.scaling_delta)).clamp(0, 3) as u8;
p.ar_coeff_lag =
(i16::from(p.ar_coeff_lag) + i16::from(self.ar_lag_delta)).clamp(0, 3) as u8;
p.grain_seed ^= self.seed_xor;
p
}
}
#[derive(Clone, Debug, Default)]
pub struct PerBlockGrainTable {
overrides: Vec<BlockGrainOverride>,
}
impl PerBlockGrainTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, entry: BlockGrainOverride) {
if let Some(e) = self
.overrides
.iter_mut()
.find(|o| o.block_col == entry.block_col && o.block_row == entry.block_row)
{
*e = entry;
} else {
self.overrides.push(entry);
}
}
#[must_use]
pub fn resolve(&self, base: &FilmGrainParams, col: u32, row: u32) -> FilmGrainParams {
for o in &self.overrides {
if o.block_col == col && o.block_row == row {
return o.apply(base);
}
}
base.clone()
}
#[must_use]
pub fn len(&self) -> usize {
self.overrides.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.overrides.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &BlockGrainOverride> {
self.overrides.iter()
}
}
#[derive(Clone, Debug, Default)]
pub struct FilmGrainTable {
entries: BTreeMap<u64, FilmGrainParams>,
default_params: FilmGrainParams,
}
impl FilmGrainTable {
#[must_use]
pub fn new() -> Self {
Self {
entries: BTreeMap::new(),
default_params: FilmGrainParams::default(),
}
}
#[must_use]
pub fn with_default(default_params: FilmGrainParams) -> Self {
Self {
entries: BTreeMap::new(),
default_params,
}
}
pub fn insert(&mut self, frame_num: u64, params: FilmGrainParams) {
self.entries.insert(frame_num, params);
}
#[must_use]
pub fn get(&self, frame_num: u64) -> Option<&FilmGrainParams> {
if let Some(params) = self.entries.get(&frame_num) {
return Some(params);
}
self.entries
.range(..=frame_num)
.next_back()
.map(|(_, params)| params)
.or(Some(&self.default_params))
}
pub fn get_mut(&mut self, frame_num: u64) -> Option<&mut FilmGrainParams> {
self.entries.get_mut(&frame_num)
}
#[must_use]
pub fn get_interpolated(&self, frame_num: u64) -> FilmGrainParams {
let prev = self.entries.range(..=frame_num).next_back();
let next = self.entries.range(frame_num..).nth(1);
match (prev, next) {
(Some((prev_frame, prev_params)), Some((next_frame, next_params))) => {
let t = if next_frame > prev_frame {
(frame_num - prev_frame) as f32 / (next_frame - prev_frame) as f32
} else {
0.0
};
interpolate_params(prev_params, next_params, t)
}
(Some((_, params)), None) => params.clone(),
(None, Some((_, params))) => params.clone(),
(None, None) => self.default_params.clone(),
}
}
pub fn remove(&mut self, frame_num: u64) -> Option<FilmGrainParams> {
self.entries.remove(&frame_num)
}
pub fn clear(&mut self) {
self.entries.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&u64, &FilmGrainParams)> {
self.entries.iter()
}
pub fn set_default(&mut self, params: FilmGrainParams) {
self.default_params = params;
}
#[must_use]
pub const fn default_params(&self) -> &FilmGrainParams {
&self.default_params
}
}
fn interpolate_params(p0: &FilmGrainParams, p1: &FilmGrainParams, t: f32) -> FilmGrainParams {
let t = t.clamp(0.0, 1.0);
let t_u8 = (t * 255.0) as u8;
let mut result = p0.clone();
result.grain_seed = lerp_u16(p0.grain_seed, p1.grain_seed, t);
for i in 0..result.num_y_points.min(p1.num_y_points) {
result.y_points[i].scaling = lerp_u8(p0.y_points[i].scaling, p1.y_points[i].scaling, t_u8);
}
result.cb_mult = lerp_u8(p0.cb_mult, p1.cb_mult, t_u8);
result.cb_luma_mult = lerp_u8(p0.cb_luma_mult, p1.cb_luma_mult, t_u8);
result.cb_offset = lerp_u16(p0.cb_offset, p1.cb_offset, t);
result.cr_mult = lerp_u8(p0.cr_mult, p1.cr_mult, t_u8);
result.cr_luma_mult = lerp_u8(p0.cr_luma_mult, p1.cr_luma_mult, t_u8);
result.cr_offset = lerp_u16(p0.cr_offset, p1.cr_offset, t);
result
}
fn lerp_u8(a: u8, b: u8, t: u8) -> u8 {
let a = u16::from(a);
let b = u16::from(b);
let t = u16::from(t);
((a * (255 - t) + b * t + 127) / 255) as u8
}
fn lerp_u16(a: u16, b: u16, t: f32) -> u16 {
let a = a as f32;
let b = b as f32;
(a + (b - a) * t) as u16
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GrainIntensity {
Light,
Medium,
Heavy,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GrainPreset {
None,
Film35mmLight,
Film35mmMedium,
Film35mmHeavy,
Film16mmLight,
Film16mmMedium,
Film16mmHeavy,
KodakVision3_500T,
KodakVision3_250D,
FujiEterna400T,
DigitalNrLight,
DigitalNrMedium,
Custom,
}
impl GrainPreset {
#[must_use]
pub fn to_params(self, bit_depth: u8) -> FilmGrainParams {
match self {
Self::None => FilmGrainParams::default(),
Self::Film35mmLight => create_35mm_params(GrainIntensity::Light, bit_depth),
Self::Film35mmMedium => create_35mm_params(GrainIntensity::Medium, bit_depth),
Self::Film35mmHeavy => create_35mm_params(GrainIntensity::Heavy, bit_depth),
Self::Film16mmLight => create_16mm_params(GrainIntensity::Light, bit_depth),
Self::Film16mmMedium => create_16mm_params(GrainIntensity::Medium, bit_depth),
Self::Film16mmHeavy => create_16mm_params(GrainIntensity::Heavy, bit_depth),
Self::KodakVision3_500T => create_kodak_vision3_500t_params(bit_depth),
Self::KodakVision3_250D => create_kodak_vision3_250d_params(bit_depth),
Self::FujiEterna400T => create_fuji_eterna_400t_params(bit_depth),
Self::DigitalNrLight => create_digital_nr_params(GrainIntensity::Light, bit_depth),
Self::DigitalNrMedium => create_digital_nr_params(GrainIntensity::Medium, bit_depth),
Self::Custom => FilmGrainParams::default(),
}
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::None => "No film grain",
Self::Film35mmLight => "35mm film - light grain (subtle cinema look)",
Self::Film35mmMedium => "35mm film - medium grain (classic cinema look)",
Self::Film35mmHeavy => "35mm film - heavy grain (gritty cinema look)",
Self::Film16mmLight => "16mm film - light grain (clean documentary look)",
Self::Film16mmMedium => "16mm film - medium grain (indie film look)",
Self::Film16mmHeavy => "16mm film - heavy grain (vintage documentary look)",
Self::KodakVision3_500T => "Kodak Vision3 500T - professional cinema film (tungsten)",
Self::KodakVision3_250D => "Kodak Vision3 250D - professional cinema film (daylight)",
Self::FujiEterna400T => "Fuji Eterna 400T - professional cinema film (tungsten)",
Self::DigitalNrLight => "Digital NR artifacts - light (subtle processing)",
Self::DigitalNrMedium => "Digital NR artifacts - medium (noticeable processing)",
Self::Custom => "Custom grain pattern",
}
}
}
fn create_35mm_params(intensity: GrainIntensity, _bit_depth: u8) -> FilmGrainParams {
let mut params = FilmGrainParams::new();
params.apply_grain = true;
params.film_grain_params_present = true;
params.grain_seed = 1234;
params.ar_coeff_lag = 2;
params.grain_scaling_minus_8 = 1;
params.ar_coeff_shift_minus_6 = 1;
params.grain_scale_shift = 0;
params.overlap_flag = true;
match intensity {
GrainIntensity::Light => {
params.add_y_point(0, 12);
params.add_y_point(64, 16);
params.add_y_point(128, 20);
params.add_y_point(192, 16);
params.add_y_point(255, 12);
}
GrainIntensity::Medium => {
params.add_y_point(0, 20);
params.add_y_point(64, 28);
params.add_y_point(128, 36);
params.add_y_point(192, 28);
params.add_y_point(255, 20);
}
GrainIntensity::Heavy => {
params.add_y_point(0, 32);
params.add_y_point(64, 44);
params.add_y_point(128, 56);
params.add_y_point(192, 44);
params.add_y_point(255, 32);
}
}
params.chroma_scaling_from_luma = false;
let chroma_scale = match intensity {
GrainIntensity::Light => [8, 12, 8],
GrainIntensity::Medium => [12, 18, 12],
GrainIntensity::Heavy => [20, 28, 20],
};
params.add_cb_point(0, chroma_scale[0]);
params.add_cb_point(128, chroma_scale[1]);
params.add_cb_point(255, chroma_scale[2]);
params.add_cr_point(0, chroma_scale[0]);
params.add_cr_point(128, chroma_scale[1]);
params.add_cr_point(255, chroma_scale[2]);
params.ar_coeffs_y[0] = 4;
params.ar_coeffs_y[1] = 3;
params.ar_coeffs_y[2] = 2;
params.ar_coeffs_y[3] = 3;
params.ar_coeffs_y[4] = 0;
params.ar_coeffs_y[5] = 2;
params.ar_coeffs_y[6] = 2;
params.ar_coeffs_y[7] = 0;
params.ar_coeffs_y[8] = 1;
params.ar_coeffs_y[9] = 3;
params.ar_coeffs_y[10] = 2;
params.ar_coeffs_y[11] = 1;
params.ar_coeffs_cb[0] = 3;
params.ar_coeffs_cb[1] = 2;
params.ar_coeffs_cb[2] = 1;
params.ar_coeffs_cb[3] = 2;
params.ar_coeffs_cb[4] = 0;
params.ar_coeffs_cb[5] = 1;
params.ar_coeffs_cb[6] = 4;
params.ar_coeffs_cr = params.ar_coeffs_cb;
params.cb_mult = 128;
params.cb_luma_mult = 192;
params.cb_offset = 256;
params.cr_mult = 128;
params.cr_luma_mult = 192;
params.cr_offset = 256;
params
}
fn create_16mm_params(intensity: GrainIntensity, _bit_depth: u8) -> FilmGrainParams {
let mut params = FilmGrainParams::new();
params.apply_grain = true;
params.film_grain_params_present = true;
params.grain_seed = 5678;
params.ar_coeff_lag = 1;
params.grain_scaling_minus_8 = 1;
params.ar_coeff_shift_minus_6 = 0;
params.grain_scale_shift = 0;
params.overlap_flag = false;
match intensity {
GrainIntensity::Light => {
params.add_y_point(0, 24);
params.add_y_point(64, 32);
params.add_y_point(128, 40);
params.add_y_point(192, 32);
params.add_y_point(255, 24);
}
GrainIntensity::Medium => {
params.add_y_point(0, 40);
params.add_y_point(64, 52);
params.add_y_point(128, 64);
params.add_y_point(192, 52);
params.add_y_point(255, 40);
}
GrainIntensity::Heavy => {
params.add_y_point(0, 56);
params.add_y_point(64, 72);
params.add_y_point(128, 88);
params.add_y_point(192, 72);
params.add_y_point(255, 56);
}
}
params.chroma_scaling_from_luma = false;
let chroma_scale = match intensity {
GrainIntensity::Light => [16, 24, 16],
GrainIntensity::Medium => [28, 36, 28],
GrainIntensity::Heavy => [40, 52, 40],
};
params.add_cb_point(0, chroma_scale[0]);
params.add_cb_point(128, chroma_scale[1]);
params.add_cb_point(255, chroma_scale[2]);
params.add_cr_point(0, chroma_scale[0]);
params.add_cr_point(128, chroma_scale[1]);
params.add_cr_point(255, chroma_scale[2]);
params.ar_coeffs_y[0] = 6;
params.ar_coeffs_y[1] = 5;
params.ar_coeffs_y[2] = 5;
params.ar_coeffs_y[3] = 6;
params.ar_coeffs_cb[0] = 5;
params.ar_coeffs_cb[1] = 4;
params.ar_coeffs_cb[2] = 4;
params.ar_coeffs_cb[3] = 5;
params.ar_coeffs_cb[4] = 6;
params.ar_coeffs_cr = params.ar_coeffs_cb;
params.cb_mult = 128;
params.cb_luma_mult = 160;
params.cb_offset = 256;
params.cr_mult = 128;
params.cr_luma_mult = 160;
params.cr_offset = 256;
params
}
fn create_kodak_vision3_500t_params(_bit_depth: u8) -> FilmGrainParams {
let mut params = FilmGrainParams::new();
params.apply_grain = true;
params.film_grain_params_present = true;
params.grain_seed = 5007;
params.ar_coeff_lag = 2;
params.grain_scaling_minus_8 = 1;
params.ar_coeff_shift_minus_6 = 1;
params.grain_scale_shift = 0;
params.overlap_flag = true;
params.add_y_point(0, 28);
params.add_y_point(32, 26);
params.add_y_point(64, 24);
params.add_y_point(96, 22);
params.add_y_point(128, 20);
params.add_y_point(160, 18);
params.add_y_point(192, 16);
params.add_y_point(224, 14);
params.add_y_point(255, 12);
params.chroma_scaling_from_luma = false;
params.add_cb_point(0, 14);
params.add_cb_point(64, 16);
params.add_cb_point(128, 18);
params.add_cb_point(192, 16);
params.add_cb_point(255, 14);
params.add_cr_point(0, 16);
params.add_cr_point(64, 18);
params.add_cr_point(128, 20);
params.add_cr_point(192, 18);
params.add_cr_point(255, 16);
params.ar_coeffs_y[0] = 5;
params.ar_coeffs_y[1] = 4;
params.ar_coeffs_y[2] = 3;
params.ar_coeffs_y[3] = 4;
params.ar_coeffs_y[4] = 2;
params.ar_coeffs_y[5] = 3;
params.ar_coeffs_y[6] = 3;
params.ar_coeffs_y[7] = 1;
params.ar_coeffs_y[8] = 2;
params.ar_coeffs_y[9] = 4;
params.ar_coeffs_y[10] = 3;
params.ar_coeffs_y[11] = 2;
params.ar_coeffs_cb[0] = 4;
params.ar_coeffs_cb[1] = 3;
params.ar_coeffs_cb[2] = 2;
params.ar_coeffs_cb[3] = 3;
params.ar_coeffs_cb[4] = 1;
params.ar_coeffs_cb[5] = 2;
params.ar_coeffs_cb[6] = 5;
params.ar_coeffs_cr = params.ar_coeffs_cb;
params.cb_mult = 120;
params.cb_luma_mult = 200;
params.cb_offset = 240;
params.cr_mult = 136;
params.cr_luma_mult = 188;
params.cr_offset = 272;
params
}
fn create_kodak_vision3_250d_params(_bit_depth: u8) -> FilmGrainParams {
let mut params = FilmGrainParams::new();
params.apply_grain = true;
params.film_grain_params_present = true;
params.grain_seed = 2507;
params.ar_coeff_lag = 2;
params.grain_scaling_minus_8 = 1;
params.ar_coeff_shift_minus_6 = 1;
params.grain_scale_shift = 0;
params.overlap_flag = true;
params.add_y_point(0, 18);
params.add_y_point(32, 17);
params.add_y_point(64, 16);
params.add_y_point(96, 15);
params.add_y_point(128, 14);
params.add_y_point(160, 13);
params.add_y_point(192, 12);
params.add_y_point(224, 11);
params.add_y_point(255, 10);
params.chroma_scaling_from_luma = false;
params.add_cb_point(0, 10);
params.add_cb_point(64, 11);
params.add_cb_point(128, 12);
params.add_cb_point(192, 11);
params.add_cb_point(255, 10);
params.add_cr_point(0, 9);
params.add_cr_point(64, 10);
params.add_cr_point(128, 11);
params.add_cr_point(192, 10);
params.add_cr_point(255, 9);
params.ar_coeffs_y[0] = 4;
params.ar_coeffs_y[1] = 3;
params.ar_coeffs_y[2] = 2;
params.ar_coeffs_y[3] = 3;
params.ar_coeffs_y[4] = 1;
params.ar_coeffs_y[5] = 2;
params.ar_coeffs_y[6] = 2;
params.ar_coeffs_y[7] = 1;
params.ar_coeffs_y[8] = 1;
params.ar_coeffs_y[9] = 3;
params.ar_coeffs_y[10] = 2;
params.ar_coeffs_y[11] = 1;
params.ar_coeffs_cb[0] = 3;
params.ar_coeffs_cb[1] = 2;
params.ar_coeffs_cb[2] = 1;
params.ar_coeffs_cb[3] = 2;
params.ar_coeffs_cb[4] = 1;
params.ar_coeffs_cb[5] = 1;
params.ar_coeffs_cb[6] = 4;
params.ar_coeffs_cr = params.ar_coeffs_cb;
params.cb_mult = 136;
params.cb_luma_mult = 188;
params.cb_offset = 272;
params.cr_mult = 120;
params.cr_luma_mult = 200;
params.cr_offset = 240;
params
}
fn create_fuji_eterna_400t_params(_bit_depth: u8) -> FilmGrainParams {
let mut params = FilmGrainParams::new();
params.apply_grain = true;
params.film_grain_params_present = true;
params.grain_seed = 4007;
params.ar_coeff_lag = 2;
params.grain_scaling_minus_8 = 1;
params.ar_coeff_shift_minus_6 = 1;
params.grain_scale_shift = 0;
params.overlap_flag = true;
params.add_y_point(0, 24);
params.add_y_point(32, 22);
params.add_y_point(64, 20);
params.add_y_point(96, 22);
params.add_y_point(128, 24);
params.add_y_point(160, 22);
params.add_y_point(192, 18);
params.add_y_point(224, 15);
params.add_y_point(255, 12);
params.chroma_scaling_from_luma = false;
params.add_cb_point(0, 12);
params.add_cb_point(64, 14);
params.add_cb_point(128, 16);
params.add_cb_point(192, 14);
params.add_cb_point(255, 12);
params.add_cr_point(0, 14);
params.add_cr_point(64, 16);
params.add_cr_point(128, 18);
params.add_cr_point(192, 16);
params.add_cr_point(255, 14);
params.ar_coeffs_y[0] = 6;
params.ar_coeffs_y[1] = 4;
params.ar_coeffs_y[2] = 3;
params.ar_coeffs_y[3] = 5;
params.ar_coeffs_y[4] = 2;
params.ar_coeffs_y[5] = 3;
params.ar_coeffs_y[6] = 4;
params.ar_coeffs_y[7] = 2;
params.ar_coeffs_y[8] = 2;
params.ar_coeffs_y[9] = 5;
params.ar_coeffs_y[10] = 3;
params.ar_coeffs_y[11] = 2;
params.ar_coeffs_cb[0] = 5;
params.ar_coeffs_cb[1] = 3;
params.ar_coeffs_cb[2] = 2;
params.ar_coeffs_cb[3] = 4;
params.ar_coeffs_cb[4] = 2;
params.ar_coeffs_cb[5] = 2;
params.ar_coeffs_cb[6] = 6;
params.ar_coeffs_cr[0] = 5;
params.ar_coeffs_cr[1] = 4;
params.ar_coeffs_cr[2] = 2;
params.ar_coeffs_cr[3] = 4;
params.ar_coeffs_cr[4] = 2;
params.ar_coeffs_cr[5] = 3;
params.ar_coeffs_cr[6] = 5;
params.cb_mult = 116;
params.cb_luma_mult = 204;
params.cb_offset = 232;
params.cr_mult = 132;
params.cr_luma_mult = 192;
params.cr_offset = 264;
params
}
fn create_digital_nr_params(intensity: GrainIntensity, _bit_depth: u8) -> FilmGrainParams {
let mut params = FilmGrainParams::new();
params.apply_grain = true;
params.film_grain_params_present = true;
params.grain_seed = 9012;
params.ar_coeff_lag = 3;
params.grain_scaling_minus_8 = 2;
params.ar_coeff_shift_minus_6 = 2;
params.grain_scale_shift = 1;
params.overlap_flag = true;
match intensity {
GrainIntensity::Light => {
params.add_y_point(0, 16);
params.add_y_point(32, 12);
params.add_y_point(96, 8);
params.add_y_point(160, 4);
params.add_y_point(224, 2);
params.add_y_point(255, 1);
}
GrainIntensity::Medium => {
params.add_y_point(0, 32);
params.add_y_point(32, 24);
params.add_y_point(96, 16);
params.add_y_point(160, 8);
params.add_y_point(224, 4);
params.add_y_point(255, 2);
}
GrainIntensity::Heavy => {
params.add_y_point(0, 48);
params.add_y_point(32, 36);
params.add_y_point(96, 24);
params.add_y_point(160, 12);
params.add_y_point(224, 6);
params.add_y_point(255, 3);
}
}
params.chroma_scaling_from_luma = true;
params.ar_coeffs_y[0] = 8;
params.ar_coeffs_y[1] = 6;
params.ar_coeffs_y[2] = 4;
params.ar_coeffs_y[3] = 6;
params.ar_coeffs_y[4] = 4;
params.ar_coeffs_y[5] = 2;
params.ar_coeffs_y[6] = 4;
params.ar_coeffs_y[7] = 2;
params.ar_coeffs_y[8] = 0;
params.ar_coeffs_y[9] = 6;
params.ar_coeffs_y[10] = 4;
params.ar_coeffs_y[11] = 2;
params.cb_mult = 128;
params.cb_luma_mult = 224;
params.cb_offset = 256;
params.cr_mult = 128;
params.cr_luma_mult = 224;
params.cr_offset = 256;
params
}
#[derive(Clone, Debug)]
pub struct GrainPatternBuilder {
params: FilmGrainParams,
}
impl GrainPatternBuilder {
#[must_use]
pub fn new() -> Self {
Self {
params: FilmGrainParams::new(),
}
}
pub fn enable(mut self) -> Self {
self.params.apply_grain = true;
self.params.film_grain_params_present = true;
self
}
pub fn seed(mut self, seed: u16) -> Self {
self.params.grain_seed = seed;
self
}
pub fn ar_lag(mut self, lag: u8) -> Self {
self.params.ar_coeff_lag = lag.min(3);
self
}
pub fn add_luma_point(mut self, value: u8, scaling: u8) -> Self {
self.params.add_y_point(value, scaling);
self
}
pub fn add_chroma_point(mut self, value: u8, scaling: u8) -> Self {
self.params.add_cb_point(value, scaling);
self.params.add_cr_point(value, scaling);
self
}
pub fn chroma_from_luma(mut self, enable: bool) -> Self {
self.params.chroma_scaling_from_luma = enable;
self
}
pub fn overlap(mut self, enable: bool) -> Self {
self.params.overlap_flag = enable;
self
}
pub fn grain_scale_shift(mut self, shift: u8) -> Self {
self.params.grain_scale_shift = shift.min(3);
self
}
#[must_use]
pub fn build(self) -> FilmGrainParams {
self.params
}
}
impl Default for GrainPatternBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_film_grain_table_creation() {
let table = FilmGrainTable::new();
assert!(table.is_empty());
assert_eq!(table.len(), 0);
}
#[test]
fn test_film_grain_table_insert_get() {
let mut table = FilmGrainTable::new();
let mut params = FilmGrainParams::new();
params.grain_seed = 1234;
table.insert(0, params.clone());
assert_eq!(table.len(), 1);
let retrieved = table.get(0).expect("get should return value");
assert_eq!(retrieved.grain_seed, 1234);
}
#[test]
fn test_film_grain_table_get_nearest() {
let mut table = FilmGrainTable::new();
let mut params0 = FilmGrainParams::new();
params0.grain_seed = 100;
table.insert(0, params0);
let mut params10 = FilmGrainParams::new();
params10.grain_seed = 200;
table.insert(10, params10);
let params5 = table.get(5).expect("get should return value");
assert_eq!(params5.grain_seed, 100);
let params15 = table.get(15).expect("get should return value");
assert_eq!(params15.grain_seed, 200);
}
#[test]
fn test_film_grain_table_interpolation() {
let mut table = FilmGrainTable::new();
let mut params0 = FilmGrainParams::new();
params0.grain_seed = 100;
params0.cb_mult = 64;
table.insert(0, params0);
let mut params10 = FilmGrainParams::new();
params10.grain_seed = 200;
params10.cb_mult = 192;
table.insert(10, params10);
let params5 = table.get_interpolated(5);
assert!(params5.grain_seed >= 100 && params5.grain_seed <= 200);
assert!(params5.cb_mult >= 64 && params5.cb_mult <= 192);
}
#[test]
fn test_film_grain_table_remove() {
let mut table = FilmGrainTable::new();
let params = FilmGrainParams::new();
table.insert(0, params);
assert_eq!(table.len(), 1);
table.remove(0);
assert_eq!(table.len(), 0);
assert!(table.is_empty());
}
#[test]
fn test_film_grain_table_clear() {
let mut table = FilmGrainTable::new();
table.insert(0, FilmGrainParams::new());
table.insert(10, FilmGrainParams::new());
assert_eq!(table.len(), 2);
table.clear();
assert_eq!(table.len(), 0);
assert!(table.is_empty());
}
#[test]
fn test_grain_presets() {
let presets = [
GrainPreset::Film35mmLight,
GrainPreset::Film35mmMedium,
GrainPreset::Film35mmHeavy,
GrainPreset::Film16mmLight,
GrainPreset::Film16mmMedium,
GrainPreset::Film16mmHeavy,
GrainPreset::KodakVision3_500T,
GrainPreset::KodakVision3_250D,
GrainPreset::FujiEterna400T,
GrainPreset::DigitalNrLight,
GrainPreset::DigitalNrMedium,
];
for preset in &presets {
let params = preset.to_params(8);
assert!(params.validate());
assert!(!preset.description().is_empty());
}
}
#[test]
fn test_35mm_light() {
let params = GrainPreset::Film35mmLight.to_params(8);
assert!(params.apply_grain);
assert!(params.film_grain_params_present);
assert!(params.num_y_points > 0);
assert!(params.validate());
}
#[test]
fn test_35mm_medium() {
let params = GrainPreset::Film35mmMedium.to_params(8);
assert!(params.num_y_points > 0);
assert!(params.overlap_flag);
assert!(params.validate());
}
#[test]
fn test_16mm_heavy() {
let params = GrainPreset::Film16mmHeavy.to_params(8);
assert!(params.num_y_points > 0);
assert!(!params.overlap_flag); assert!(params.validate());
}
#[test]
fn test_digital_nr() {
let params = GrainPreset::DigitalNrMedium.to_params(8);
assert!(params.num_y_points > 0);
assert!(params.chroma_scaling_from_luma);
assert!(params.validate());
}
#[test]
fn test_grain_pattern_builder() {
let params = GrainPatternBuilder::new()
.enable()
.seed(12345)
.ar_lag(2)
.add_luma_point(0, 32)
.add_luma_point(128, 48)
.add_luma_point(255, 32)
.add_chroma_point(0, 16)
.add_chroma_point(255, 16)
.chroma_from_luma(false)
.overlap(true)
.grain_scale_shift(0)
.build();
assert!(params.apply_grain);
assert_eq!(params.grain_seed, 12345);
assert_eq!(params.ar_coeff_lag, 2);
assert_eq!(params.num_y_points, 3);
assert!(params.overlap_flag);
assert!(params.validate());
}
#[test]
fn test_lerp_u8() {
assert_eq!(lerp_u8(0, 255, 0), 0);
assert_eq!(lerp_u8(0, 255, 255), 255);
let mid = lerp_u8(0, 255, 128);
assert!(mid >= 127 && mid <= 128);
}
#[test]
fn test_lerp_u16() {
assert_eq!(lerp_u16(0, 1000, 0.0), 0);
assert_eq!(lerp_u16(0, 1000, 1.0), 1000);
let mid = lerp_u16(0, 1000, 0.5);
assert!(mid >= 499 && mid <= 501);
}
#[test]
fn test_interpolate_params() {
let mut p0 = FilmGrainParams::new();
p0.grain_seed = 100;
p0.cb_mult = 64;
let mut p1 = FilmGrainParams::new();
p1.grain_seed = 200;
p1.cb_mult = 192;
let mid = interpolate_params(&p0, &p1, 0.5);
assert!(mid.grain_seed >= 100 && mid.grain_seed <= 200);
assert!(mid.cb_mult >= 64 && mid.cb_mult <= 192);
}
#[test]
fn test_bit_depth_scaling() {
let params_8 = GrainPreset::Film35mmMedium.to_params(8);
let params_10 = GrainPreset::Film35mmMedium.to_params(10);
assert!(params_8.validate());
assert!(params_10.validate());
}
#[test]
fn test_table_with_default() {
let mut default_params = FilmGrainParams::new();
default_params.grain_seed = 9999;
let table = FilmGrainTable::with_default(default_params);
let params = table.get(100).expect("get should return value");
assert_eq!(params.grain_seed, 9999);
}
#[test]
fn test_table_iteration() {
let mut table = FilmGrainTable::new();
table.insert(0, FilmGrainParams::new());
table.insert(10, FilmGrainParams::new());
table.insert(20, FilmGrainParams::new());
let count = table.iter().count();
assert_eq!(count, 3);
}
#[test]
fn test_kodak_vision3_500t() {
let params = GrainPreset::KodakVision3_500T.to_params(8);
assert!(params.apply_grain);
assert!(params.film_grain_params_present);
assert_eq!(params.grain_seed, 5007);
assert!(params.num_y_points > 0);
assert!(params.num_cb_points > 0);
assert!(params.num_cr_points > 0);
assert!(params.overlap_flag);
assert!(params.validate());
}
#[test]
fn test_kodak_vision3_250d() {
let params = GrainPreset::KodakVision3_250D.to_params(8);
assert!(params.apply_grain);
assert!(params.film_grain_params_present);
assert_eq!(params.grain_seed, 2507);
assert!(params.num_y_points > 0);
assert!(params.num_cb_points > 0);
assert!(params.num_cr_points > 0);
assert!(params.overlap_flag);
assert!(params.validate());
}
#[test]
fn test_fuji_eterna_400t() {
let params = GrainPreset::FujiEterna400T.to_params(8);
assert!(params.apply_grain);
assert!(params.film_grain_params_present);
assert_eq!(params.grain_seed, 4007);
assert!(params.num_y_points > 0);
assert!(params.num_cb_points > 0);
assert!(params.num_cr_points > 0);
assert!(params.overlap_flag);
assert!(params.validate());
}
#[test]
fn test_film_stock_grain_characteristics() {
let v250d = GrainPreset::KodakVision3_250D.to_params(8);
let v500t = GrainPreset::KodakVision3_500T.to_params(8);
assert!(v250d.y_points[0].scaling < v500t.y_points[0].scaling);
let eterna = GrainPreset::FujiEterna400T.to_params(8);
assert!(eterna.num_y_points >= 5);
}
#[test]
fn test_film_stock_bit_depth_scaling() {
for bit_depth in [8, 10, 12] {
let v500t = GrainPreset::KodakVision3_500T.to_params(bit_depth);
let v250d = GrainPreset::KodakVision3_250D.to_params(bit_depth);
let eterna = GrainPreset::FujiEterna400T.to_params(bit_depth);
assert!(v500t.validate());
assert!(v250d.validate());
assert!(eterna.validate());
}
}
#[test]
fn test_per_block_grain_table_empty() {
let table = PerBlockGrainTable::new();
assert!(table.is_empty());
assert_eq!(table.len(), 0);
}
#[test]
fn test_per_block_grain_override_no_op() {
let base = GrainPreset::Film35mmMedium.to_params(8);
let table = PerBlockGrainTable::new();
let resolved = table.resolve(&base, 0, 0);
assert_eq!(
resolved.grain_scaling_minus_8, base.grain_scaling_minus_8,
"No override should return unchanged scaling"
);
assert_eq!(
resolved.grain_seed, base.grain_seed,
"No override should return unchanged seed"
);
}
#[test]
fn test_per_block_grain_override_scaling_delta() {
let base = GrainPreset::Film35mmMedium.to_params(8);
let mut table = PerBlockGrainTable::new();
let mut ovr = BlockGrainOverride::new(3, 5);
ovr.scaling_delta = 1;
table.set(ovr);
let resolved = table.resolve(&base, 3, 5);
let expected = (base.grain_scaling_minus_8 as i16 + 1).clamp(0, 3) as u8;
assert_eq!(
resolved.grain_scaling_minus_8, expected,
"scaling_delta=+1 must increment grain_scaling_minus_8"
);
}
#[test]
fn test_per_block_grain_override_seed_xor() {
let base = GrainPreset::Film35mmMedium.to_params(8);
let mut table = PerBlockGrainTable::new();
let mut ovr = BlockGrainOverride::new(1, 2);
ovr.seed_xor = 0xABCD;
table.set(ovr);
let resolved = table.resolve(&base, 1, 2);
assert_eq!(
resolved.grain_seed,
base.grain_seed ^ 0xABCD,
"seed_xor must XOR the base grain seed"
);
}
#[test]
fn test_per_block_grain_override_ar_lag_clamped() {
let base = GrainPreset::Film35mmMedium.to_params(8);
let mut table = PerBlockGrainTable::new();
let mut ovr = BlockGrainOverride::new(0, 0);
ovr.ar_lag_delta = 10;
table.set(ovr);
let resolved = table.resolve(&base, 0, 0);
assert!(
resolved.ar_coeff_lag <= 3,
"ar_coeff_lag must be clamped to [0, 3], got {}",
resolved.ar_coeff_lag
);
}
#[test]
fn test_per_block_grain_different_blocks_independent() {
let base = GrainPreset::Film35mmMedium.to_params(8);
let mut table = PerBlockGrainTable::new();
let mut ovr_a = BlockGrainOverride::new(0, 0);
ovr_a.scaling_delta = 1;
ovr_a.seed_xor = 0x1234;
table.set(ovr_a);
let mut ovr_b = BlockGrainOverride::new(2, 4);
ovr_b.scaling_delta = -1;
ovr_b.seed_xor = 0x5678;
table.set(ovr_b);
let r_a = table.resolve(&base, 0, 0);
let r_b = table.resolve(&base, 2, 4);
let expected_a = (base.grain_scaling_minus_8 as i16 + 1).clamp(0, 3) as u8;
assert_eq!(r_a.grain_scaling_minus_8, expected_a);
let expected_b = (base.grain_scaling_minus_8 as i16 - 1).clamp(0, 3) as u8;
assert_eq!(r_b.grain_scaling_minus_8, expected_b);
assert_ne!(
r_a.grain_seed, r_b.grain_seed,
"Different blocks must have different seeds"
);
}
#[test]
fn test_per_block_grain_override_replaced_on_set() {
let base = GrainPreset::Film16mmMedium.to_params(8);
let mut table = PerBlockGrainTable::new();
let mut ovr1 = BlockGrainOverride::new(1, 1);
ovr1.seed_xor = 0x0001;
table.set(ovr1);
assert_eq!(table.len(), 1);
let mut ovr2 = BlockGrainOverride::new(1, 1);
ovr2.seed_xor = 0x0002;
table.set(ovr2);
assert_eq!(
table.len(),
1,
"Override at same coords must replace, not duplicate"
);
let r = table.resolve(&base, 1, 1);
assert_eq!(
r.grain_seed,
base.grain_seed ^ 0x0002,
"Latest override seed_xor must be used"
);
}
#[test]
fn test_per_block_grain_iter() {
let mut table = PerBlockGrainTable::new();
for i in 0..5u32 {
table.set(BlockGrainOverride::new(i, i));
}
assert_eq!(table.iter().count(), 5);
}
#[test]
fn test_per_block_grain_non_override_returns_base() {
let base = GrainPreset::DigitalNrLight.to_params(8);
let mut table = PerBlockGrainTable::new();
table.set(BlockGrainOverride::new(7, 7));
let r = table.resolve(&base, 0, 0);
assert_eq!(r.grain_seed, base.grain_seed);
assert_eq!(r.grain_scaling_minus_8, base.grain_scaling_minus_8);
}
#[test]
fn test_per_block_grain_fidelity_spatial_variation() {
let base = GrainPreset::Film35mmHeavy.to_params(8);
let mut table = PerBlockGrainTable::new();
for row in 0..4u32 {
for col in 0..4u32 {
let dist_from_center = ((row as i32 - 2).abs() + (col as i32 - 2).abs()) as i8;
let mut ovr = BlockGrainOverride::new(col, row);
ovr.scaling_delta = (dist_from_center / 2).min(2);
ovr.seed_xor = (row * 17 + col * 31) as u16;
table.set(ovr);
}
}
let center = table.resolve(&base, 2, 2);
let corner = table.resolve(&base, 0, 0);
assert!(
corner.grain_scaling_minus_8 >= center.grain_scaling_minus_8,
"Corner grain should be >= center grain: corner={}, center={}",
corner.grain_scaling_minus_8,
center.grain_scaling_minus_8
);
for row in 0..4u32 {
for col in 0..4u32 {
let r = table.resolve(&base, col, row);
assert!(
r.validate(),
"Per-block override must produce valid grain params at ({col},{row})"
);
}
}
}
}