Color Lookup: an extreme .3dl shaper can't bake NaN into the table

CodeRabbit finding on #9170 ("Check the baked table for non-finite values"):
a finite but extreme shaper such as 0 1e308 -1e308 0 overflowed to infinity
when interpolated over the resampled grid, and infinity times 0 put NaN in the
table. The shaper is scaled to 0..1 before interpolating, and the baked table
is checked like any parsed one. New test:
three_dl_extreme_shaper_bakes_only_finite_values (NaN without the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hector VillarrealandClaude Opus 5.5 committed 2026-10-30 12:57:09 -06:00
1 parent 759edaa3c7
commit 216b53301f
1 file changed
+20 -7
+20 -7
View File
@@ -320,7 +320,7 @@ pub fn parse_3dl(text: &str) -> Result<LutFile, LutError> {
}
let lut = LutFile { title: String::new(), size: n, data, domain_min: [0.0; 3], domain_max: [1.0; 3] }.check()?;
Ok(match shaper {
Some(shaper) => apply_shaper(lut, &shaper),
Some(shaper) => apply_shaper(lut, &shaper)?,
None => lut,
})
}
@@ -331,10 +331,10 @@ pub fn parse_3dl(text: &str) -> Result<LutFile, LutError> {
/// value at the shaped input. An even ramp (the usual shaper) changes nothing and is skipped.
/// The table is baked with at least as many nodes as the shaper has points (up to [`MAX_SIZE`]):
/// exact for an identity table, an approximation at that resolution otherwise.
fn apply_shaper(lut: LutFile, shaper: &[f64]) -> LutFile {
fn apply_shaper(lut: LutFile, shaper: &[f64]) -> Result<LutFile, LutError> {
let m = shaper.len();
if m < 2 || shaper.iter().any(|v| !v.is_finite()) {
return lut;
return Ok(lut);
}
// The shaper's own depth, from 10 bits up: a 0..1023 ramp is the even ramp with or without a header.
let scale = inferred_3dl_scale(shaper.iter().fold(0.0f64, |a, &b| a.max(b)), true);
@@ -343,14 +343,16 @@ fn apply_shaper(lut: LutFile, shaper: &[f64]) -> LutFile {
// is 1; a float shaper (0..1) is held to one 10-bit step.
let code = if scale > 1.0 { 1.0 } else { 1.0 / 1023.0 };
if shaper.iter().enumerate().all(|(k, &v)| (v - even(k)).abs() <= code) {
return lut;
return Ok(lut);
}
// Scaled to 0..1 first, so interpolating between two extreme values can't overflow.
let shaped: Vec<f64> = shaper.iter().map(|v| v / scale).collect();
let curve = |x: f64| -> f32 {
let p = x.clamp(0.0, 1.0) * (m - 1) as f64;
let i = (p.floor() as usize).min(m - 2);
let f = p - i as f64;
let (a, b) = (shaper.get(i).copied().unwrap_or(0.0), shaper.get(i + 1).copied().unwrap_or(0.0));
((a + (b - a) * f) / scale).clamp(0.0, 1.0) as f32
let (a, b) = (shaped.get(i).copied().unwrap_or(0.0), shaped.get(i + 1).copied().unwrap_or(0.0));
(a + (b - a) * f).clamp(0.0, 1.0) as f32
};
// A shaper finer than the table is baked at the shaper's resolution (up to MAX_SIZE), so its
// curve survives a small table: a 2-node table would otherwise read it only at its two ends.
@@ -368,7 +370,7 @@ fn apply_shaper(lut: LutFile, shaper: &[f64]) -> LutFile {
}
}
}
out
out.check()
}
/// Trilinear lookup of `rgb` (0..1) in a [`LutFile`] (red fastest).
@@ -590,6 +592,17 @@ mod tests {
assert_eq!(sample(&small, [1.0, 0.0, 1.0]), [1.0, 0.0, 1.0], "the ends stay put");
}
/// #9170 review: a finite but extreme shaper can't overflow into NaN when it is baked.
#[test]
fn three_dl_extreme_shaper_bakes_only_finite_values() {
let table = identity_3dl(&[0, 1, 2, 3], 2);
let text = format!("0 1e308 -1e308 0\n{}", table.split_once('\n').unwrap().1);
// Refused or baked, but never a table holding NaN.
if let Ok(l) = parse_3dl(&text) {
assert!(l.data.iter().all(|v| v.is_finite()), "baked a non-finite value");
}
}
#[test]
fn cube_title_with_hash_round_trips() {
let l = LutFile::from_fn("Look #3", 2, |c| c);