mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
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:
co-authored by
Claude Haiku 5.5
parent
8ef5337c29
commit
d722dd8b54
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user