#![allow(clippy::excessive_precision)]
use super::*;
use crate::transforms::{GroupTransform, Transform};
use crate::types::{FixedFunctionStyle, OptimizationFlags, TransformDirection};
fn equal_with_safe_rel_error(act: f32, aim: f32, tol: f32, min_expected: f32) -> (bool, f32) {
let div = if aim.abs() > min_expected {
aim.abs()
} else {
min_expected
};
let err = (act - aim).abs() / div;
(err <= tol, err)
}
#[test]
fn registry_basic() {
let mut registry = BuiltinTransformRegistry::new();
assert_eq!(registry.num_builtins(), 0);
assert_eq!(
registry.builtin_style(0).unwrap_err().message(),
"Invalid index."
);
let mut ops = Vec::new();
assert_eq!(
registry
.create_transforms(0, &mut ops)
.unwrap_err()
.message(),
"Invalid index."
);
registry.add_builtin("trans1", "", |_ops| Ok(()));
assert_eq!(registry.num_builtins(), 1);
assert!(registry
.builtin_style(0)
.unwrap()
.eq_ignore_ascii_case("trans1"));
registry.add_builtin("TRANS1", "", |_ops| Ok(()));
assert_eq!(registry.num_builtins(), 1);
assert!(registry
.builtin_style(0)
.unwrap()
.eq_ignore_ascii_case("trans1"));
registry.create_transforms(0, &mut ops).unwrap();
assert!(ops.is_empty());
}
fn create_transforms(name: &str) -> Vec<Transform> {
let reg = BuiltinTransformRegistry::get();
let index = reg
.index_of(name)
.unwrap_or_else(|| panic!("Unknown built-in transform name '{name}'."));
let mut v = Vec::new();
reg.create_transforms(index, &mut v).unwrap();
v
}
#[test]
fn registry_aces() {
let t = create_transforms("IDENTITY");
assert_eq!(t.len(), 1);
let Transform::Matrix(m) = &t[0] else {
panic!("expected a matrix")
};
assert_eq!(m.matrix, crate::transforms::IDENTITY_MATRIX44);
assert_eq!(m.offset, [0.0; 4]);
let t = create_transforms("UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD");
assert_eq!(t.len(), 1);
assert!(matches!(t[0], Transform::Matrix(_)));
let t = create_transforms("CURVE - ACEScct-LOG_to_LINEAR");
assert_eq!(t.len(), 1);
let Transform::LogCamera(l) = &t[0] else {
panic!("expected a log camera")
};
assert_eq!(l.direction, TransformDirection::Inverse);
assert_eq!(l.base, 2.0);
assert_eq!(l.lin_side_break, [0.0078125; 3]);
assert_eq!(l.log_side_slope, [1.0 / 17.52; 3]);
assert_eq!(l.log_side_offset, [9.72 / 17.52; 3]);
}
#[test]
fn registry_aces_ops() {
use crate::config::Config;
let config = Config::create_raw();
let context = config.current_context().clone();
let build = |style: &str| {
let mut ops = crate::ops::OpVec::new();
BuiltinTransform::new(style)
.build_ops(&mut ops, &config, &context, TransformDirection::Forward)
.unwrap();
ops
};
let ops = build("IDENTITY");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].name(), "Matrix");
let ops = build("UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].name(), "Matrix");
let ops = build("CURVE - ACEScct-LOG_to_LINEAR");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].name(), "Log");
}
const CONFIG_BUILTIN_TRANSFORMS: &str = r#"ocio_profile_version: 2.6
environment:
{}
search_path: ""
strictparsing: true
luma: [0.2126, 0.7152, 0.0722]
roles:
aces_interchange: test
color_timing: test
compositing_log: test
default: ref
scene_linear: test
file_rules:
- !<Rule> {name: Default, colorspace: default}
displays:
Disp1:
- !<View> {name: View1, colorspace: test}
active_displays: []
active_views: []
colorspaces:
- !<ColorSpace>
name: ref
family: ""
equalitygroup: ""
bitdepth: unknown
isdata: false
allocation: uniform
- !<ColorSpace>
name: test
family: ""
equalitygroup: ""
bitdepth: unknown
isdata: false
allocation: uniform
from_scene_reference: !<GroupTransform>
children:"#;
#[test]
fn registry_read_write() {
use crate::config::Config;
let mut config_str = CONFIG_BUILTIN_TRANSFORMS.to_string();
for style in builtin_transform_styles() {
config_str += "\n - !<BuiltinTransform> {style: ";
config_str += style;
config_str += "}";
}
config_str += "\n";
let config = Config::create_from_str(&config_str).unwrap();
config.validate().unwrap();
assert_eq!(config.serialize().unwrap(), config_str);
config.get_processor("ref", "test").unwrap();
}
#[test]
fn registry_version_1_validation() {
const CONFIG: &str = r#"ocio_profile_version: 1
search_path: ""
strictparsing: true
luma: [0.2126, 0.7152, 0.0722]
roles:
default: ref
displays:
Disp1:
- !<View> {name: View1, colorspace: test}
colorspaces:
- !<ColorSpace>
name: ref
- !<ColorSpace>
name: test
to_reference: !<BuiltinTransform> {style: ACEScct_to_ACES2065-1}"#;
assert_eq!(
crate::config::Config::create_from_str(CONFIG)
.unwrap_err()
.message(),
"Only config version 2 (or higher) can have BuiltinInTransform."
);
}
fn version_config(version: &str, style: &str) -> String {
format!(
r#"ocio_profile_version: {version}
environment:
{{}}
search_path: ""
strictparsing: true
luma: [0.2126, 0.7152, 0.0722]
roles:
default: ref
file_rules:
- !<Rule> {{name: Default, colorspace: default}}
displays:
Disp1:
- !<View> {{name: View1, colorspace: test}}
active_displays: []
active_views: []
colorspaces:
- !<ColorSpace>
name: ref
- !<ColorSpace>
name: test
from_scene_reference: !<BuiltinTransform> {{style: {style}}}"#
)
}
#[test]
fn registry_version_2_validation() {
let cfg = version_config("2", "ACES-LMT - ACES 1.3 Reference Gamut Compression");
assert_eq!(
crate::config::Config::create_from_str(&cfg)
.unwrap_err()
.message(),
"Only config version 2.1 (or higher) can have BuiltinTransform style \
'ACES-LMT - ACES 1.3 Reference Gamut Compression'."
);
}
#[test]
fn registry_version_2_1_validation() {
let cfg = version_config("2.1", "ARRI_LOGC4_to_ACES2065-1");
assert_eq!(
crate::config::Config::create_from_str(&cfg).unwrap_err().message(),
"Only config version 2.2 (or higher) can have BuiltinTransform style 'ARRI_LOGC4_to_ACES2065-1'."
);
}
const STYLES_2_4: &[&str] = &[
"APPLE_LOG_to_ACES2065-1",
"CURVE - APPLE_LOG_to_LINEAR",
"CURVE - HLG-OETF",
"CURVE - HLG-OETF-INVERSE",
"DISPLAY - CIE-XYZ-D65_to_DCDM-D65",
"DISPLAY - CIE-XYZ-D65_to_ST2084-DCDM-D65",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-108nit-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-300nit-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-REC2020_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-REC2020_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-REC2020_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-REC2020_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-REC709-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-REC2020-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D60-in-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D60-in-XYZ-E_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-108nit-P3-D60-in-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-300nit-P3-D60-in-XYZ-E_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D60-in-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D60-in-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D60-in-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D60-in-P3-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D60-in-REC2020-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D60-in-REC2020-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D60-in-REC2020-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D60-in-REC2020-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-REC2020-D60-in-REC2020-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-REC2020-D60-in-REC2020-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-REC2020-D60-in-REC2020-D65_2.0",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-REC2020-D60-in-REC2020-D65_2.0",
];
#[test]
fn registry_styles_2_4_exist() {
for style in STYLES_2_4 {
assert!(
BuiltinTransformRegistry::get().index_of(style).is_some(),
"{style}"
);
}
}
#[test]
fn registry_version_2_3_validation() {
for style in STYLES_2_4 {
let cfg = version_config("2.3", style);
assert_eq!(
crate::config::Config::create_from_str(&cfg)
.unwrap_err()
.message(),
format!(
"Only config version 2.4 (or higher) can have BuiltinTransform style '{style}'."
)
);
}
}
#[test]
fn creation() {
let mut blt = BuiltinTransform::default();
assert_eq!(blt.direction, TransformDirection::Forward);
assert_eq!(blt.canonical_style().unwrap(), "IDENTITY");
blt.validate().unwrap();
blt.set_style("UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD")
.unwrap();
assert_eq!(blt.style, "UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD");
blt.validate().unwrap();
assert_eq!(
blt.description().unwrap(),
"Convert ACES AP0 primaries to CIE XYZ with a D65 white point with Bradford adaptation"
);
blt.direction = TransformDirection::Inverse;
assert_eq!(blt.direction, TransformDirection::Inverse);
blt.validate().unwrap();
blt.set_style("UTILITY - ACES-AP0_to_cie-xyz-D65_BFD")
.unwrap();
blt.validate().unwrap();
assert_eq!(blt.style, "UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD");
let blt2 = BuiltinTransform::new("utility - aces-ap0_to_cie-xyz-d65_bfd");
blt2.validate().unwrap();
assert_eq!(
blt2.canonical_style().unwrap(),
"UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD"
);
let msg = "BuiltinTransform: invalid built-in transform style 'UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD_UNKNOWN'.";
assert_eq!(
blt.set_style("UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD_UNKNOWN")
.unwrap_err()
.message(),
msg
);
assert_eq!(blt.style, "UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD");
let unknown = BuiltinTransform::new("UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD_UNKNOWN");
assert_eq!(unknown.validate().unwrap_err().message(), msg);
assert_eq!(
Transform::Builtin(unknown)
.validate()
.unwrap_err()
.message(),
msg
);
}
#[test]
fn access() {
assert_eq!(builtin_transform_style(0).unwrap(), "IDENTITY");
assert_eq!(
builtin_transform_style(1).unwrap(),
"UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD"
);
assert_eq!(
builtin_transform_description_by_index(1).unwrap(),
"Convert ACES AP0 primaries to CIE XYZ with a D65 white point with Bradford adaptation"
);
assert_eq!(
builtin_transform_description("UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD"),
Some(
"Convert ACES AP0 primaries to CIE XYZ with a D65 white point with Bradford adaptation"
)
);
assert_eq!(builtin_transform_description("unknown"), None);
assert_eq!(builtin_transform_description("IDENTITY"), Some(""));
assert_eq!(builtin_transform_styles().len(), num_builtin_transforms());
assert!(builtin_transform_style(num_builtin_transforms()).is_err());
assert!(builtin_transforms("unknown").is_err());
}
#[test]
fn group_transform() {
let mut blt = BuiltinTransform::new("ACEScct_to_ACES2065-1");
let g = blt.to_group_transform().unwrap();
assert_eq!(g.direction, TransformDirection::Forward);
assert_eq!(g.transforms.len(), 2);
assert!(matches!(g.transforms[0], Transform::LogCamera(_)));
assert!(matches!(g.transforms[1], Transform::Matrix(_)));
blt.direction = TransformDirection::Inverse;
let g = blt.to_group_transform().unwrap();
assert_eq!(g.direction, TransformDirection::Inverse);
}
#[test]
fn forward_inverse() {
use crate::config::Config;
let fwd = BuiltinTransform::new("ACEScct_to_ACES2065-1");
fwd.validate().unwrap();
let mut inv = BuiltinTransform::new("ACEScct_to_ACES2065-1");
inv.direction = TransformDirection::Inverse;
inv.validate().unwrap();
let mut grp = GroupTransform::new();
grp.append(fwd);
grp.append(inv);
assert_eq!(grp.num_transforms(), 2);
let config = Config::create_raw();
let proc = config
.get_processor_for_transform(&Transform::Group(grp), TransformDirection::Forward)
.unwrap();
let g = proc
.optimized(OptimizationFlags::NONE)
.create_group_transform();
assert_eq!(g.num_transforms(), 4);
let g = proc
.optimized(OptimizationFlags::DEFAULT)
.create_group_transform();
assert_eq!(g.num_transforms(), 0);
}
type TestValues = (&'static str, f32, &'static [f32], &'static [f32]);
#[rustfmt::skip]
const UNIT_TEST_VALUES: &[TestValues] = &[
("IDENTITY", 1.0e-6, &[0.5, 0.4, 0.3], &[0.5, 0.4, 0.3]),
("UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD", 1.0e-6, &[0.5, 0.4, 0.3], &[0.472347603390, 0.440425934827, 0.326581044758]),
("UTILITY - ACES-AP1_to_CIE-XYZ-D65_BFD", 1.0e-6, &[0.5, 0.4, 0.3], &[0.428407900093, 0.420968434905, 0.325777868096]),
("UTILITY - ACES-AP1_to_LINEAR-REC709_BFD", 1.0e-6, &[0.5, 0.4, 0.3], &[0.578830986466, 0.388029190156, 0.282302431033]),
("CURVE - ACEScct-LOG_to_LINEAR", 1.0e-6, &[0.5, 0.4, 0.3], &[0.514056913328, 0.152618314084, 0.045310838527]),
("ACEScct_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.386397222658, 0.158557251811, 0.043152537925]),
("ACEScc_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.386398554, 0.158557251811, 0.043152537925]),
("ACEScg_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.453158317919, 0.394926024520, 0.299297344519]),
("ACESproxy10i_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.433437174444, 0.151629880817, 0.031769555400]),
("ADX10_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.210518101020, 0.148655364394, 0.085189053481]),
("ADX16_to_ACES2065-1", 1.0e-6, &[0.125, 0.1, 0.075], &[0.211320835792, 0.149169650771, 0.085452970479]),
("ACES-LMT - BLUE_LIGHT_ARTIFACT_FIX", 1.0e-6, &[0.5, 0.4, 0.3], &[0.48625676579, 0.38454173877, 0.30002108779]),
("ACES-LMT - ACES 1.3 Reference Gamut Compression", 1.0e-6, &[0.5, 0.4, -0.3], &[0.54812347889, 0.42805567384, -0.00588858686]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA_1.0", 1.0e-6, &[0.5, 0.4, 0.3], &[0.33629957, 0.31832799, 0.22867827]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-VIDEO_1.0", 1.0e-6, &[0.5, 0.4, 0.3], &[0.34128153, 0.32533440, 0.24217427]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-REC709lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.33629954, 0.31832793, 0.22867827]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-VIDEO-REC709lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.34128147, 0.32533434, 0.24217427]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-VIDEO-P3lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.34128150, 0.32533440, 0.24217424]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-D60sim-D65_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.32699189, 0.30769098, 0.20432013]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-VIDEO-D60sim-D65_1.0", 1.0e-6, &[0.5, 0.4, 0.3], &[0.32889283, 0.31174013, 0.21453267]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-D60sim-DCI_1.0", 1.0e-6, &[0.5, 0.4, 0.3], &[0.34226444, 0.30731421, 0.23189434]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-D65sim-DCI_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.33882778, 0.30572337, 0.24966924]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-1000nit-15nit-REC2020lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.48334542, 0.45336276, 0.32364485]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-1000nit-15nit-P3lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.48334542, 0.45336276, 0.32364485]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-2000nit-15nit-REC2020lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.50538367, 0.47084737, 0.32972121]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-2000nit-15nit-P3lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.50538367, 0.47084737, 0.32972121]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-4000nit-15nit-REC2020lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.52311981, 0.48482567, 0.33447576]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-4000nit-15nit-P3lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.52311981, 0.48482567, 0.33447576]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-CINEMA-108nit-7.2nit-P3lim_1.1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.22214814, 0.21179835, 0.15639816]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.26260215, 0.25207460, 0.20617345]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.26260215, 0.25207475, 0.20617352]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-108nit-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.16253395, 0.15513620, 0.12449738]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-300nit-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.20592400, 0.19440512, 0.15028587]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.41039270, 0.38813815, 0.30191854]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.46536559, 0.43852845, 0.33688101]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.51225948, 0.48264498, 0.37060043]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.55653530, 0.51967967, 0.38678783]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-REC2020_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.41039288, 0.38813818, 0.30191860]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-REC2020_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.46536580, 0.43852842, 0.33688098]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-REC2020_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.51225960, 0.48264492, 0.37060046]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-REC2020_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.55653548, 0.51967967, 0.38678783]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-REC709-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.25147712, 0.24029461, 0.18221153]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.25373834, 0.24245527, 0.18384993]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.25712875, 0.24569492, 0.18630651]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D60-in-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.25373828, 0.24245520, 0.18384989]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D60-in-XYZ-E_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.26332238, 0.25161314, 0.19079420]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-108nit-P3-D60-in-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.15705051, 0.14920059, 0.11100878]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-300nit-P3-D60-in-XYZ-E_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.20469207, 0.19229385, 0.13782671]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D60-in-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.39655733, 0.37322620, 0.26917258]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D60-in-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.44968122, 0.42165339, 0.30032712]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D60-in-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.49499470, 0.46407115, 0.33038712]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D60-in-P3-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.53778988, 0.49960214, 0.34477147]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.40185603, 0.37821317, 0.27276924]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.45568976, 0.42728746, 0.30434006]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.50160873, 0.47027206, 0.33480173]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.54497570, 0.50627774, 0.34937829]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-REC2020-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.40185642, 0.37821338, 0.27276939]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-REC2020-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.45569009, 0.42728764, 0.30434042]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-REC2020-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.50160891, 0.47027206, 0.33480188]),
("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-REC2020-D60-in-REC2020-D65_2.0", 1.0e-4, &[0.5, 0.4, 0.3], &[0.54497600, 0.50627792, 0.34937853]),
("APPLE_LOG_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.153334766, 0.083515430, 0.032948254]),
("APPLE_LOG-APPLEWG_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.160302015, 0.091223177, 0.026405713]),
("CURVE - APPLE_LOG_to_LINEAR", 1.0e-6, &[0.5, 0.4, 0.3], &[0.198913991, 0.083076466024, 0.0315782763]),
("ARRI_ALEXA-LOGC-EI800-AWG_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.401621427766, 0.236455447604, 0.064830001192]),
("ARRI_LOGC4_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[1.786878082249, 0.743018593362, 0.232840037656]),
("CANON_CLOG2-CGAMUT_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.408435767126, 0.197486903378, 0.034204558318]),
("CURVE - CANON_CLOG2_to_LINEAR", 1.0e-6, &[0.5, 0.4, 0.3], &[0.492082215086, 0.183195624930, 0.064213555991]),
("CANON_CLOG3-CGAMUT_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.496034919950, 0.301015360499, 0.083691829261]),
("CURVE - CANON_CLOG3_to_LINEAR", 1.0e-6, &[0.5, 0.4, 0.3], &[0.580777404788, 0.282284436009, 0.122823721131]),
("PANASONIC_VLOG-VGAMUT_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.306918773245, 0.148128050597, 0.046334439047]),
("RED_REDLOGFILM-RWG_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.216116808829, 0.121529105934, 0.008171766322]),
("RED_LOG3G10-RWG_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.887988237100, 0.416932247547, -0.025442210717]),
("SONY_SLOG3-SGAMUT3_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.342259707137, 0.172043362337, 0.057188031769]),
("SONY_SLOG3-SGAMUT3.CINE_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.314942672433, 0.170408017753, 0.046854940520]),
("SONY_SLOG3-SGAMUT3-VENICE_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.35101694, 0.17165215, 0.05479717]),
("SONY_SLOG3-SGAMUT3.CINE-VENICE_to_ACES2065-1", 1.0e-6, &[0.5, 0.4, 0.3], &[0.32222527, 0.17032611, 0.04477848]),
("DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.709", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.937245093108, 0.586817090358, 0.573498106368, 0., 0.505174310421, 1.118456082347]),
("DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.709 - MIRROR NEGS", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.937245093108, 0.586817090358, 0.573498106368, -0.940082660458, 0.505174310421, 1.118456082347]),
("DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.2020", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.830338272693, 0.620393283803, 0.583385370254, 0., 0.432629991358, 1.069355537167]),
("DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.2020 - MIRROR NEGS", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.830338272693, 0.620393283803, 0.583385370254, -0.696883299726, 0.432629991358, 1.069355537167]),
("DISPLAY - CIE-XYZ-D65_to_G2.2-REC.709", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.931739212204, 0.559058879141, 0.545230761999, 0., 0.474767926071, 1.129896956592]),
("DISPLAY - CIE-XYZ-D65_to_G2.2-REC.709 - MIRROR NEGS", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.931739212204, 0.559058879141, 0.545230761999, -0.934816978533, 0.474767926071, 1.129896956592]),
("DISPLAY - CIE-XYZ-D65_to_sRGB", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.933793573229, 0.564092030327, 0.550040502218, -11.142147651136028, 0.477958897494, 1.124971166876]),
("DISPLAY - CIE-XYZ-D65_to_sRGB - MIRROR NEGS", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.933793573229, 0.564092030327, 0.550040502218, -0.936787206783, 0.477958897494, 1.124971166876]),
("DISPLAY - CIE-XYZ-D65_to_G2.6-P3-DCI-BFD", 1.0e-6, &[0.5, 0.4, 0.3], &[0.908856342287, 0.627840575107, 0.608053675805]),
("DISPLAY - CIE-XYZ-D65_to_G2.6-P3-D65", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.896805202281, 0.627254277624, 0.608228132100, 0., 0.493163009212, 1.069368427937]),
("DISPLAY - CIE-XYZ-D65_to_G2.6-P3-D65 - MIRROR NEGS", 1.0e-6, &[0.5, 0.4, 0.3, -0.05, 0.05, 1.25], &[0.896805202281, 0.627254277624, 0.608228132100, -0.859521292874, 0.493163009212, 1.069368427937]),
("DISPLAY - CIE-XYZ-D65_to_G2.6-P3-D60-BFD", 1.0e-6, &[0.5, 0.4, 0.3], &[0.892433142142, 0.627011653770, 0.608093643982]),
("DISPLAY - CIE-XYZ-D65_to_DCDM-D65", 1.0e-6, &[0.5, 0.4, 0.3], &[0.740738422348, 0.679816639411, 0.608609083713]),
("DISPLAY - CIE-XYZ-D65_to_DisplayP3", 1.0e-6, &[0.5, 0.4, 0.3], &[0.882580907776, 0.581526360743, 0.5606367050000]),
("DISPLAY - CIE-XYZ-D65_to_DisplayP3-HDR", 1.0e-6, &[0.5, 0.4, 0.3], &[0.882580907776, 0.581526360743, 0.5606367050000]),
("CURVE - ST-2084_to_LINEAR", 4.0e-5, &[0.5, 0.4, 0.3, -0.1, -0.3, 1.01], &[0.922457089941, 0.324479178538, 0.100382263105, -0.0032456566, -0.10038226, 110.045776]),
("CURVE - LINEAR_to_ST-2084", 1.0e-5, &[0.5, 0.4, 0.3, -0.1, 101.0, 0.2], &[0.440281573420, 0.419284117712, 0.392876186489, -0.299699098, 1.00104129, 0.357012421]),
("DISPLAY - CIE-XYZ-D65_to_REC.2100-PQ", 1.0e-5, &[0.5, 0.4, 0.3, -0.1, 1.01, 0.2], &[0.464008302136, 0.398157119110, 0.384828370950, -0.454744577, 0.562376201, 0.328883916]),
("DISPLAY - CIE-XYZ-D65_to_ST2084-P3-D65", 1.0e-5, &[0.5, 0.4, 0.3, -0.1, 1.01, 0.2], &[0.479939091128, 0.392091860770, 0.384886051856, -0.532302439, 0.572011411, 0.307887018]),
("DISPLAY - CIE-XYZ-D65_to_ST2084-DCDM-D65", 1.0e-6, &[0.5, 0.4, 0.3], &[0.440281573420, 0.419284117712, 0.392876186489]),
("CURVE - HLG-OETF-INVERSE", 1.0e-5, &[0.5, 0.4, 0.3, -0.7, 1.2, 0.9], &[0.25, 0.16, 0.09, -0.618367240391, 9.032932830300, 1.745512772886]),
("CURVE - HLG-OETF", 1.0e-5, &[0.5, 0.4, 0.3, -0.1, 10.0, 0.2], &[0.656409985167, 0.608926718364, 0.544089493962, -0.316227766017, 1.218326006877, 0.4472135955]),
("DISPLAY - CIE-XYZ-D65_to_REC.2100-HLG-1000nit", 6.0e-5, &[0.5, 0.4, 0.3, -0.1, 1.01, 0.2], &[0.5649694, 0.4038837, 0.3751478, -0.505630434, 0.738133013, 0.251128823]),
];
#[test]
fn validate_values_cover_all_builtins() {
let reg = BuiltinTransformRegistry::get();
for style in reg.styles() {
let v = UNIT_TEST_VALUES.iter().find(|v| v.0 == style);
let Some((_, _, input, output)) = v else {
panic!("For the built-in transform '{style}' the values are missing.");
};
assert_eq!(
input.len(),
output.len(),
"For the built-in transform '{style}' the input and output values do not match."
);
assert_eq!(
input.len() % 3,
0,
"For the built-in transform '{style}' only RGB values are supported."
);
}
assert_eq!(UNIT_TEST_VALUES.len(), reg.num_builtins());
for style in reg.styles() {
let t = builtin_transforms(style).unwrap();
assert!(!t.is_empty(), "{style}");
let blt = BuiltinTransform::new(style);
blt.validate().unwrap();
let g = blt.to_group_transform().unwrap();
assert_eq!(format!("{:?}", g.transforms), format!("{t:?}"));
}
}
fn validate_builtin_transform(style: &str, input: &[f32], output: &[f32], error_threshold: f32) {
use crate::config::Config;
let mut blt = BuiltinTransform::default();
blt.set_style(style).unwrap();
blt.validate().unwrap();
let config = Config::create_raw();
let proc = config
.get_processor_for_transform(&Transform::Builtin(blt), TransformDirection::Forward)
.unwrap();
let cpu = proc.optimized_cpu_processor(OptimizationFlags::LOSSLESS);
let mut results = input.to_vec();
cpu.apply_rgb_slice(&mut results);
for (idx, (&act, &aim)) in results.iter().zip(output.iter()).enumerate() {
let (ok, err) = equal_with_safe_rel_error(act, aim, error_threshold, 1.0);
assert!(
ok,
"{style}: for index = {idx} - Values: {act} expected: {aim} - Error: {err} ({}x of Threshold: {error_threshold})",
err / error_threshold
);
}
}
#[test]
fn validate() {
let reg = BuiltinTransformRegistry::get();
for style in reg.styles() {
let (_, tol, input, output) = UNIT_TEST_VALUES.iter().find(|v| v.0 == style).unwrap();
validate_builtin_transform(style, input, output, *tol);
}
assert_eq!(UNIT_TEST_VALUES.len(), reg.num_builtins());
}
fn validate_display_view_round_trip(
display_style: &str,
view_style: &str,
scale: f32,
error_threshold: f32,
apply_lmt: bool,
difficult_items: &[usize],
difficult_threshold: f32,
) {
use crate::config::Config;
let display = BuiltinTransform::new(display_style);
display.validate().unwrap();
let mut display_inv = display.clone();
display_inv.direction = TransformDirection::Inverse;
let view = BuiltinTransform::new(view_style);
view.validate().unwrap();
let mut view_inv = view.clone();
view_inv.direction = TransformDirection::Inverse;
let look = BuiltinTransform::new("ACES-LMT - BLUE_LIGHT_ARTIFACT_FIX");
look.validate().unwrap();
let mut look_inv = look.clone();
look_inv.direction = TransformDirection::Inverse;
let mut group = GroupTransform::new();
group.append(display_inv);
group.append(view_inv);
if apply_lmt {
group.append(look_inv);
group.append(look);
}
group.append(view);
group.append(display);
let config = Config::create_raw();
let proc = config
.get_processor_for_transform(&Transform::Group(group), TransformDirection::Forward)
.unwrap();
let cpu = proc.optimized_cpu_processor(OptimizationFlags::NONE);
const LUT_SIZE: usize = 7;
let num_samples = LUT_SIZE * LUT_SIZE * LUT_SIZE;
let c = 1.0f32 / (LUT_SIZE as f32 - 1.0);
let mut input = vec![0.0f32; num_samples * 4];
for i in 0..num_samples {
input[4 * i] = (i % LUT_SIZE) as f32 * c;
input[4 * i + 1] = ((i / LUT_SIZE) % LUT_SIZE) as f32 * c;
input[4 * i + 2] = ((i / LUT_SIZE / LUT_SIZE) % LUT_SIZE) as f32 * c;
}
for v in input.iter_mut() {
*v *= scale;
}
let mut output = input.clone();
cpu.apply_rgba_slice(&mut output);
for idx in (0..num_samples * 4).step_by(4) {
let tol = if difficult_items.contains(&idx) {
difficult_threshold
} else {
error_threshold
};
let (r, er) = equal_with_safe_rel_error(output[idx], input[idx], tol, 1.0);
let (g, eg) = equal_with_safe_rel_error(output[idx + 1], input[idx + 1], tol, 1.0);
let (b, eb) = equal_with_safe_rel_error(output[idx + 2], input[idx + 2], tol, 1.0);
assert!(
r && g && b,
"Index: {idx} - Tol.: {tol}\n - Expected: {}, {}, {}\n - Actual: {}, {}, {}\n - Error: {er}, {eg}, {eb}",
input[idx],
input[idx + 1],
input[idx + 2],
output[idx],
output[idx + 1],
output[idx + 2]
);
}
}
#[test]
fn aces2_displayview_roundtrip() {
validate_display_view_round_trip(
"DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.709",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709_2.0",
1.0,
0.004,
false,
&[],
0.0,
);
validate_display_view_round_trip(
"DISPLAY - CIE-XYZ-D65_to_DisplayP3",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D65_2.0",
1.0,
0.001,
false,
&[],
0.0,
);
validate_display_view_round_trip(
"DISPLAY - CIE-XYZ-D65_to_ST2084-P3-D65",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D65_2.0",
0.7507,
0.005,
false,
&[168, 196, 364, 392, 1344],
0.03,
);
validate_display_view_round_trip(
"DISPLAY - CIE-XYZ-D65_to_ST2084-P3-D65",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D65_2.0",
0.8987,
0.007,
false,
&[
168, 196, 392, 396, 588, 592, 952, 1148, 1196, 1200, 1260, 1288,
],
0.2,
);
validate_display_view_round_trip(
"DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.709",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709_2.0",
1.0,
0.004,
true,
&[192, 1344],
0.006,
);
validate_display_view_round_trip(
"DISPLAY - CIE-XYZ-D65_to_DisplayP3",
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D65_2.0",
1.0,
0.001,
true,
&[],
0.0,
);
}
#[test]
fn aces2_aab_to_rgb_nan() {
use crate::config::Config;
let mut display_inv = BuiltinTransform::new("DISPLAY - CIE-XYZ-D65_to_ST2084-P3-D65");
display_inv.direction = TransformDirection::Inverse;
let mut view_inv = BuiltinTransform::new(
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D60-in-P3-D65_2.0",
);
view_inv.direction = TransformDirection::Inverse;
let mut group = GroupTransform::new();
group.append(display_inv);
group.append(view_inv);
let config = Config::create_raw();
let proc = config
.get_processor_for_transform(&Transform::Group(group), TransformDirection::Forward)
.unwrap();
let cpu = proc.default_cpu_processor();
let mut pixel = [0.89942779f32, 0.89942779, 0.89942779];
cpu.apply_rgb(&mut pixel);
assert!(!pixel[0].is_nan());
assert!(!pixel[1].is_nan());
assert!(!pixel[2].is_nan());
}
#[test]
fn builtin_structure() {
use crate::types::{GradingStyle, NegativeStyle, RgbCurveType};
let t = builtin_transforms("ACEScc_to_ACES2065-1").unwrap();
assert_eq!(t.len(), 4);
let Transform::Range(r) = &t[0] else { panic!() };
assert_eq!(
(r.min_in, r.max_in, r.min_out, r.max_out),
(Some(-0.36), Some(1.5), Some(0.0), Some(1.0))
);
let Transform::Lut1D(lut) = &t[1] else {
panic!()
};
assert_eq!(lut.length(), 4096);
assert!(!lut.input_half_domain);
let x = 2048.0 / 4095.0 * (1.50 - -0.36) + -0.36;
assert_eq!(lut.value(2048)[0], (2.0f64.powf(x * 17.52 - 9.72)) as f32);
let x = 0.0 * (1.50 - -0.36) + -0.36;
assert_eq!(
lut.value(0)[0],
((2.0f64.powf(x * 17.52 - 9.72) - 2.0f64.powf(-16.0)) * 2.0) as f32
);
let Transform::Range(r) = &t[3] else { panic!() };
assert_eq!((r.min_in, r.max_in), (Some(0.0), None));
let t = builtin_transforms("ADX10_to_ACES2065-1").unwrap();
assert_eq!(t.len(), 5);
let Transform::Matrix(m) = &t[0] else {
panic!()
};
assert_eq!(m.matrix[0], 1023.0 / 500.0);
assert_eq!(m.offset, [-95.0 / 500.0, -95.0 / 500.0, -95.0 / 500.0, 0.0]);
let Transform::Lut1D(lut) = &t[2] else {
panic!()
};
assert!(lut.input_half_domain);
assert_eq!(lut.length(), 65536);
let Transform::Log(l) = &t[3] else { panic!() };
assert_eq!((l.base, l.direction), (10.0, TransformDirection::Inverse));
let t = builtin_transforms("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA_1.0").unwrap();
let curves: Vec<_> = t
.iter()
.filter(|x| matches!(x, Transform::GradingRgbCurve(_)))
.collect();
assert_eq!(curves.len(), 2);
let Transform::GradingRgbCurve(c) = curves[1] else {
panic!()
};
assert_eq!(c.style, GradingStyle::Log);
let master = c.value.curve(RgbCurveType::Master);
assert_eq!(master.num_control_points(), 15);
assert_eq!(master.slope(14), 0.04);
assert_eq!(c.value.curve(RgbCurveType::Red).num_control_points(), 2);
let t = builtin_transforms(
"ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-108nit-P3-D60-in-P3-D65_2.0",
)
.unwrap();
assert_eq!(t.len(), 8);
let Transform::Matrix(m) = &t[2] else {
panic!()
};
assert_eq!(m.direction, TransformDirection::Inverse);
let Transform::FixedFunction(ff) = &t[3] else {
panic!()
};
assert_eq!(ff.style, FixedFunctionStyle::AcesOutputTransform20);
assert_eq!(
ff.params,
vec![225.0, 0.680, 0.320, 0.265, 0.690, 0.150, 0.060, 0.32168, 0.33767]
);
let Transform::Range(r) = &t[4] else { panic!() };
assert_eq!(r.max_in, Some(2.25));
let Transform::Matrix(m) = &t[6] else {
panic!()
};
assert_eq!(m.matrix[0], 0.48f32 as f64);
let Transform::Range(r) = &t[1] else { panic!() };
let upper = (8.0 * (128.0 + 768.0 * ((2.25f32).ln() as f64 / 100.0f64.ln()))) as f32;
assert_eq!(r.max_in, Some(upper as f64));
let t = builtin_transforms("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709_2.0")
.unwrap();
assert_eq!(t.len(), 6);
let t = builtin_transforms("CURVE - APPLE_LOG_to_LINEAR").unwrap();
assert_eq!(t.len(), 1);
let Transform::Lut1D(lut) = &t[0] else {
panic!()
};
assert!(lut.input_half_domain);
assert_eq!(lut.value(0xbc00)[0], -0.05641088f32);
let t = builtin_transforms("CURVE - CANON_CLOG3_to_LINEAR").unwrap();
let Transform::Lut1D(lut) = &t[0] else {
panic!()
};
assert_eq!(lut.length(), 4096);
assert!(!lut.input_half_domain);
let t = builtin_transforms("RED_REDLOGFILM-RWG_to_ACES2065-1").unwrap();
let Transform::LogAffine(l) = &t[0] else {
panic!()
};
assert_eq!((l.base, l.direction), (10.0, TransformDirection::Inverse));
let t = builtin_transforms("SONY_SLOG3-SGAMUT3_to_ACES2065-1").unwrap();
let Transform::LogCamera(l) = &t[0] else {
panic!()
};
assert!(l.linear_slope.is_some());
let t = builtin_transforms("DISPLAY - CIE-XYZ-D65_to_sRGB - MIRROR NEGS").unwrap();
let Transform::ExponentWithLinear(e) = &t[1] else {
panic!()
};
assert_eq!(e.gamma, [2.4, 2.4, 2.4, 1.0]);
assert_eq!(e.offset, [0.055, 0.055, 0.055, 0.0]);
assert_eq!(e.negative_style, NegativeStyle::Mirror);
assert_eq!(e.direction, TransformDirection::Inverse);
let t = builtin_transforms("DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.709").unwrap();
let Transform::Exponent(e) = &t[1] else {
panic!()
};
assert_eq!(e.value, [2.4, 2.4, 2.4, 1.0]);
assert_eq!(e.negative_style, NegativeStyle::Clamp);
assert_eq!(e.direction, TransformDirection::Inverse);
let t = builtin_transforms("DISPLAY - CIE-XYZ-D65_to_REC.2100-HLG-1000nit").unwrap();
assert_eq!(t.len(), 5);
let Transform::FixedFunction(ff) = &t[3] else {
panic!()
};
assert_eq!(ff.style, FixedFunctionStyle::Rec2100Surround);
assert_eq!(ff.params, vec![1.0 / 1.2]);
let Transform::Lut1D(lut) = &t[4] else {
panic!()
};
assert!(lut.input_half_domain);
let t = builtin_transforms("CURVE - ST-2084_to_LINEAR").unwrap();
let Transform::Lut1D(lut) = &t[0] else {
panic!()
};
assert!(lut.input_half_domain);
assert!((lut.value(0x3c00)[0] - 100.0).abs() < 1e-3);
}
#[test]
fn half_luts() {
let t = builtin_transforms("ADX16_to_ACES2065-1").unwrap();
let Transform::Lut1D(lut) = &t[2] else {
panic!()
};
let idx = half::f16::from_f32(100.0).to_bits() as usize;
assert_eq!(lut.value(idx)[0], 4.8162678f32);
let idx = half::f16::from_f32(-100.0).to_bits() as usize;
assert_eq!(lut.value(idx)[0], -10.0);
let idx = half::f16::from_f32(0.5).to_bits() as usize;
assert_eq!(lut.value(idx)[0], -1.121718645f64 as f32);
let t =
builtin_transforms("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-D60sim-DCI_1.0")
.unwrap();
let lut = t
.iter()
.find_map(|x| {
if let Transform::Lut1D(l) = x {
Some(l)
} else {
None
}
})
.unwrap();
assert_eq!(
lut.value(half::f16::from_f32(0.25).to_bits() as usize)[0],
0.25
);
assert!((lut.value(half::f16::from_f32(1.0).to_bits() as usize)[0] - 0.918).abs() < 1e-6);
}
mod reference_eval {
use crate::transforms::Transform;
use crate::types::{NegativeStyle, TransformDirection};
fn apply_matrix(m: &[f64; 16], o: &[f64; 4], p: &[f64; 3]) -> [f64; 3] {
let mut out = [0.0; 3];
for (i, v) in out.iter_mut().enumerate() {
*v = m[i * 4] * p[0] + m[i * 4 + 1] * p[1] + m[i * 4 + 2] * p[2] + o[i];
}
out
}
fn lut_lookup(values: &[f32], half_domain: bool, c: usize, x: f64) -> f64 {
let n = values.len() / 3;
let at = |i: usize| values[3 * i + c] as f64;
if half_domain {
let h = half::f16::from_f64(x);
let hv = h.to_f64();
let (lo, hi) = if hv <= x {
(h, next_up(h))
} else {
(next_down(h), h)
};
let (lv, hv) = (lo.to_f64(), hi.to_f64());
let (li, hi_i) = (lo.to_bits() as usize, hi.to_bits() as usize);
if hv == lv {
return at(li);
}
let t = (x - lv) / (hv - lv);
at(li) + (at(hi_i) - at(li)) * t
} else {
let pos = (x.clamp(0.0, 1.0)) * (n - 1) as f64;
let i0 = pos.floor() as usize;
let i1 = (i0 + 1).min(n - 1);
let t = pos - i0 as f64;
at(i0) + (at(i1) - at(i0)) * t
}
}
fn next_up(h: half::f16) -> half::f16 {
let b = h.to_bits();
if h.to_f64() >= 0.0 && b & 0x8000 == 0 {
half::f16::from_bits(b + 1)
} else if b == 0x8000 {
half::f16::from_bits(1)
} else {
half::f16::from_bits(b - 1)
}
}
fn next_down(h: half::f16) -> half::f16 {
let b = h.to_bits();
if b == 0 {
half::f16::from_bits(0x8001)
} else if b & 0x8000 == 0 {
half::f16::from_bits(b - 1)
} else {
half::f16::from_bits(b + 1)
}
}
struct Camera {
base: f64,
log_slope: f64,
log_offset: f64,
lin_slope: f64,
lin_offset: f64,
brk: Option<f64>,
linear_slope: Option<f64>,
}
impl Camera {
fn log_break(&self, brk: f64) -> f64 {
self.log_slope * (self.lin_slope * brk + self.lin_offset).log(self.base)
+ self.log_offset
}
fn linear_slope(&self, brk: f64) -> f64 {
self.linear_slope.unwrap_or_else(|| {
self.log_slope * self.lin_slope
/ ((self.lin_slope * brk + self.lin_offset) * self.base.ln())
})
}
fn fwd(&self, x: f64) -> f64 {
if let Some(brk) = self.brk {
if x <= brk {
return self.linear_slope(brk) * (x - brk) + self.log_break(brk);
}
}
self.log_slope * (self.lin_slope * x + self.lin_offset).log(self.base) + self.log_offset
}
fn inv(&self, y: f64) -> f64 {
if let Some(brk) = self.brk {
let lb = self.log_break(brk);
if y <= lb {
return (y - lb) / self.linear_slope(brk) + brk;
}
}
(self.base.powf((y - self.log_offset) / self.log_slope) - self.lin_offset)
/ self.lin_slope
}
}
fn moncurve_rev(x: f64, g: f64, o: f64, style: NegativeStyle) -> f64 {
let brk_code = o / (g - 1.0);
let brk_lin = ((brk_code + o) / (1.0 + o)).powf(g);
let slope = brk_code / brk_lin;
let f = |x: f64| {
if x <= brk_lin {
x * slope
} else {
(1.0 + o) * x.powf(1.0 / g) - o
}
};
match style {
NegativeStyle::Mirror => x.signum() * f(x.abs()),
_ => f(x),
}
}
pub(super) fn eval(transforms: &[Transform], rgb: [f64; 3]) -> Option<[f64; 3]> {
use TransformDirection::{Forward, Inverse};
let mut p = rgb;
for t in transforms {
p = match t {
Transform::Matrix(m) => match m.direction {
Forward => apply_matrix(&m.matrix, &m.offset, &p),
Inverse => {
let inv = crate::math_utils::m44_inverse(&m.matrix)?;
let q = [p[0] - m.offset[0], p[1] - m.offset[1], p[2] - m.offset[2]];
apply_matrix(&inv, &[0.0; 4], &q)
}
},
Transform::Range(r) => {
let (min_in, max_in) = (r.min_in, r.max_in);
let (min_out, max_out) = (r.min_out, r.max_out);
p.map(|x| {
let scale = match (min_in, max_in, min_out, max_out) {
(Some(a), Some(b), Some(c), Some(d)) => (d - c) / (b - a),
_ => 1.0,
};
let offset = match (min_in, min_out, max_in, max_out) {
(Some(a), Some(c), _, _) => c - a * scale,
(_, _, Some(b), Some(d)) => d - b * scale,
_ => 0.0,
};
let mut y = x * scale + offset;
if let Some(c) = min_out {
y = y.max(c);
}
if let Some(d) = max_out {
y = y.min(d);
}
y
})
}
Transform::Log(l) => match l.direction {
Forward => p.map(|x| x.log(l.base)),
Inverse => p.map(|x| l.base.powf(x)),
},
Transform::LogCamera(l) => {
let c = Camera {
base: l.base,
log_slope: l.log_side_slope[0],
log_offset: l.log_side_offset[0],
lin_slope: l.lin_side_slope[0],
lin_offset: l.lin_side_offset[0],
brk: Some(l.lin_side_break[0]),
linear_slope: l.linear_slope.map(|s| s[0]),
};
match l.direction {
Forward => p.map(|x| c.fwd(x)),
Inverse => p.map(|x| c.inv(x)),
}
}
Transform::LogAffine(l) => {
let c = Camera {
base: l.base,
log_slope: l.log_side_slope[0],
log_offset: l.log_side_offset[0],
lin_slope: l.lin_side_slope[0],
lin_offset: l.lin_side_offset[0],
brk: None,
linear_slope: None,
};
match l.direction {
Forward => p.map(|x| c.fwd(x)),
Inverse => p.map(|x| c.inv(x)),
}
}
Transform::Exponent(e) if e.direction == Inverse => {
let g = e.value[0];
match e.negative_style {
NegativeStyle::Mirror => p.map(|x| x.signum() * x.abs().powf(1.0 / g)),
_ => p.map(|x| x.max(0.0).powf(1.0 / g)),
}
}
Transform::ExponentWithLinear(e) if e.direction == Inverse => {
p.map(|x| moncurve_rev(x, e.gamma[0], e.offset[0], e.negative_style))
}
Transform::Lut1D(l) if l.direction == Forward => {
let mut out = [0.0; 3];
for c in 0..3 {
out[c] = lut_lookup(&l.values, l.input_half_domain, c, p[c]);
}
out
}
_ => return None,
};
}
Some(p)
}
}
#[test]
fn reference_values_with_reference_evaluator() {
let mut checked = Vec::new();
for (style, tol, input, output) in UNIT_TEST_VALUES {
let transforms = builtin_transforms(style).unwrap();
for (i, (inp, out)) in input.chunks(3).zip(output.chunks(3)).enumerate() {
let rgb = [inp[0] as f64, inp[1] as f64, inp[2] as f64];
let Some(res) = reference_eval::eval(&transforms, rgb) else {
break;
};
for c in 0..3 {
let (ok, err) = equal_with_safe_rel_error(res[c] as f32, out[c], 4.0 * tol, 1.0);
assert!(
ok,
"{style}: pixel {i} channel {c}: {} expected {} (error {err})",
res[c], out[c]
);
}
if i == 0 {
checked.push(*style);
}
}
}
for style in [
"IDENTITY",
"UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD",
"UTILITY - ACES-AP1_to_CIE-XYZ-D65_BFD",
"UTILITY - ACES-AP1_to_LINEAR-REC709_BFD",
"CURVE - ACEScct-LOG_to_LINEAR",
"ACEScct_to_ACES2065-1",
"ACEScc_to_ACES2065-1",
"ACEScg_to_ACES2065-1",
"ACESproxy10i_to_ACES2065-1",
"ADX10_to_ACES2065-1",
"ADX16_to_ACES2065-1",
"ACES-LMT - BLUE_LIGHT_ARTIFACT_FIX",
"ARRI_ALEXA-LOGC-EI800-AWG_to_ACES2065-1",
"ARRI_LOGC4_to_ACES2065-1",
"PANASONIC_VLOG-VGAMUT_to_ACES2065-1",
"RED_REDLOGFILM-RWG_to_ACES2065-1",
"RED_LOG3G10-RWG_to_ACES2065-1",
"SONY_SLOG3-SGAMUT3_to_ACES2065-1",
"SONY_SLOG3-SGAMUT3.CINE_to_ACES2065-1",
"SONY_SLOG3-SGAMUT3-VENICE_to_ACES2065-1",
"SONY_SLOG3-SGAMUT3.CINE-VENICE_to_ACES2065-1",
"DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.709",
"DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.709 - MIRROR NEGS",
"DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.2020",
"DISPLAY - CIE-XYZ-D65_to_REC.1886-REC.2020 - MIRROR NEGS",
"DISPLAY - CIE-XYZ-D65_to_G2.2-REC.709",
"DISPLAY - CIE-XYZ-D65_to_G2.2-REC.709 - MIRROR NEGS",
"DISPLAY - CIE-XYZ-D65_to_sRGB",
"DISPLAY - CIE-XYZ-D65_to_sRGB - MIRROR NEGS",
"DISPLAY - CIE-XYZ-D65_to_G2.6-P3-DCI-BFD",
"DISPLAY - CIE-XYZ-D65_to_G2.6-P3-D65",
"DISPLAY - CIE-XYZ-D65_to_G2.6-P3-D65 - MIRROR NEGS",
"DISPLAY - CIE-XYZ-D65_to_G2.6-P3-D60-BFD",
"DISPLAY - CIE-XYZ-D65_to_DCDM-D65",
"DISPLAY - CIE-XYZ-D65_to_DisplayP3",
"DISPLAY - CIE-XYZ-D65_to_DisplayP3-HDR",
] {
assert!(checked.contains(&style), "{style} not checked");
}
}