Clamp filter.cameraRaw exposure to its documented -5..5 range (#6876)

Found while testing crates/algo/src/camera_raw.rs:592 (value taken unchecked in crates/engine/src/lens_cmds.rs:137 raw_params)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-18 03:07:03 -04:00
committed by GitHub
co-authored by Claude Haiku 5.5
parent 8ef5337c29
commit d722dd8b54
2 changed files with 43 additions and 0 deletions
+3
View File
@@ -147,6 +147,9 @@ pub fn raw_params(cmd: &str, p: &Value) -> Result<CameraRaw> {
cr.noise_luminance_detail = cr.noise_luminance_detail.clamp(0.0, 100.0);
cr.noise_color = cr.noise_color.clamp(0.0, 100.0);
cr.noise_color_detail = cr.noise_color_detail.clamp(0.0, 100.0);
// `exposure` becomes `2^exposure`, which overflows f32 to infinity from 128 up (and then
// writes inf/NaN into 32-bit layers). Clamp to the documented -5..5.
cr.exposure = cr.exposure.clamp(-5.0, 5.0);
Ok(cr)
}
@@ -0,0 +1,40 @@
//! `filter.cameraRaw` must clamp `exposure` to its documented -5..5 range. Before the fix,
//! `raw_params` passed it through, so an exposure of 128 or more overflowed `2^exposure` to
//! infinity and a 32-bit layer was left with non-finite samples.
use photocraft_engine::Session;
use serde_json::json;
#[test]
fn camera_raw_clamps_exposure() {
let hi = photocraft_engine::lens_cmds::raw_params("filter.cameraRaw", &json!({"exposure": 255.0})).expect("params parse");
assert_eq!(hi.exposure, 5.0, "exposure 255 must clamp to 5");
let lo = photocraft_engine::lens_cmds::raw_params("filter.cameraRaw", &json!({"exposure": -255.0})).expect("params parse");
assert_eq!(lo.exposure, -5.0, "exposure -255 must clamp to -5");
let mid = photocraft_engine::lens_cmds::raw_params("filter.cameraRaw", &json!({"exposure": 1.5})).expect("params parse");
assert_eq!(mid.exposure, 1.5, "in-range exposure stays untouched");
}
#[test]
fn camera_raw_out_of_range_exposure_keeps_32_bit_layer_finite() {
for exposure in [255.0, -255.0] {
let mut s = Session::new();
s.execute("file.new", json!({"width": 8, "height": 8, "depth": 32})).unwrap();
s.execute("layer.new.layer", json!({})).unwrap();
s.execute("edit.fill", json!({"contents": "color", "color": "#ff0000"})).unwrap();
s.execute("filter.cameraRaw", json!({"exposure": exposure})).unwrap();
let st = s.active().unwrap();
let layer = st.active_layer.and_then(|id| st.doc.layer(id)).unwrap();
let surf = layer.surface().unwrap();
let mut bad = Vec::new();
for y in 0..8 {
for x in 0..8 {
let px = surf.rgba(x, y);
if px.iter().any(|v| !v.is_finite()) {
bad.push(px);
}
}
}
assert!(bad.is_empty(), "exposure {exposure}: {} non-finite pixels, e.g. {:?}", bad.len(), bad.first());
}
}