Legacy Brightness/Contrast: brightness first, then contrast around 128, as in Photoshop (#2615)
* Legacy Brightness/Contrast: brightness first, then contrast around 128, as in Photoshop With Use Legacy on, PhotoCraft applied contrast first and then added the brightness, so any setting that moves both sliders came out wrong: brightness +40 with contrast +30 mapped mid-grey 128 to 168, where Photoshop gives 185 (up to 17 levels darker across the midtones). Measured on Photoshop 27.11 (scripted, a gray ramp through seven settings): Photoshop adds the brightness, then scales contrast around 128/255. Contrast 80 keeps 128 at 128; the 0.5 pivot moved it to 130. With both changes every measured value matches within 1 level, on the CPU and in the GPU adjustment kernel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Legacy B/C: brightness after contrast when contrast is lowered The corpus caught it: psd-tools' brightnesscontrast_legacy_grayscale.psd (brightness -23, contrast -26) matched Photoshop with the old order and was 6 levels off with brightness first. Photoshop adds the brightness before contrast when contrast is raised (measured: +40/+30 maps 128 to 185) and after it when contrast is lowered. The measured cases with lowered contrast had brightness 0, so they could not tell. CPU and GPU kernel (brightness after contrast goes in p0.w). The io corpus oracle is back at its floor; the 34 GPU parity tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b077cc3a18
commit
2c163bf73d
|
|
@ -85,12 +85,16 @@ pub fn apply_depth(adj: &Adjustment, buf: &mut Buffer, transfer: Transfer, depth
|
|||
}
|
||||
Adjustment::Posterize { levels } => map_rgb(buf, |c| c.map(|v| posterize(v, *levels))),
|
||||
Adjustment::BrightnessContrast { brightness, contrast, legacy: true } => {
|
||||
// Legacy: contrast scales around mid-grey, then brightness is added
|
||||
// (fitted to Photoshop's rendering, exact on the corpus).
|
||||
// Legacy: contrast scales around 128/255, with brightness added before it when
|
||||
// contrast is raised and after it when contrast is lowered. Measured on Photoshop
|
||||
// 27.11, brightness +40 with contrast +30 maps 128 to 185 (brightness first), while
|
||||
// the corpus's brightness -23 with contrast -26 (psd-tools) needs contrast first;
|
||||
// contrast 80 keeps 128 at 128 (a 0.5 pivot moved it to 130).
|
||||
let b = brightness / 255.0;
|
||||
let c = contrast.clamp(-100.0, 99.0);
|
||||
let k = if c >= 0.0 { 1.0 / (1.0 - c / 100.0) } else { 1.0 + c / 100.0 };
|
||||
map_rgb(buf, |px| px.map(|v| ((v - 0.5) * k + 0.5 + b).clamp(0.0, 1.0)))
|
||||
let (pre, post) = if c >= 0.0 { (b, 0.0) } else { (0.0, b) };
|
||||
map_rgb(buf, |px| px.map(|v| ((v + pre - LEGACY_PIVOT) * k + LEGACY_PIVOT + post).clamp(0.0, 1.0)))
|
||||
}
|
||||
Adjustment::BrightnessContrast { brightness, contrast, .. } => {
|
||||
// Modern (CS3+) Brightness/Contrast, reverse-engineered from Photoshop ground truth
|
||||
|
|
@ -820,6 +824,32 @@ mod tone_tests {
|
|||
Buffer { rect, px }
|
||||
}
|
||||
|
||||
/// Photoshop 27.11, measured: Brightness/Contrast with Use Legacy on, over a gray ramp
|
||||
/// (x = 0, 16, …, 240, 255), one row per (brightness, contrast).
|
||||
#[test]
|
||||
fn legacy_brightness_contrast_matches_photoshop() {
|
||||
const X: [u8; 17] = [0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 255];
|
||||
let measured: [(f32, f32, [u8; 17]); 7] = [
|
||||
(40.0, 0.0, [40, 56, 72, 88, 104, 120, 136, 152, 168, 184, 200, 216, 232, 248, 255, 255, 255]),
|
||||
(-40.0, 0.0, [0, 0, 0, 8, 24, 40, 56, 72, 88, 104, 120, 136, 152, 168, 184, 200, 215]),
|
||||
(0.0, 30.0, [0, 0, 0, 14, 37, 60, 82, 105, 128, 151, 174, 196, 219, 242, 255, 255, 255]),
|
||||
(0.0, -30.0, [38, 49, 61, 72, 83, 94, 106, 117, 128, 139, 150, 162, 173, 184, 195, 207, 217]),
|
||||
(40.0, 30.0, [3, 25, 48, 71, 94, 117, 139, 162, 185, 208, 231, 253, 255, 255, 255, 255, 255]),
|
||||
(100.0, 0.0, [100, 116, 132, 148, 164, 180, 196, 212, 228, 244, 255, 255, 255, 255, 255, 255, 255]),
|
||||
(0.0, 80.0, [0, 0, 0, 0, 0, 0, 0, 48, 128, 208, 255, 255, 255, 255, 255, 255, 255]),
|
||||
];
|
||||
for (brightness, contrast, want) in measured {
|
||||
let rect = Rect::new(0, 0, 17, 1);
|
||||
let px = X.iter().map(|x| [f32::from(*x) / 255.0; 4]).map(|p| [p[0], p[1], p[2], 1.0]).collect();
|
||||
let mut b = Buffer { rect, px };
|
||||
apply(&Adjustment::BrightnessContrast { brightness, contrast, legacy: true }, &mut b);
|
||||
for (i, (p, w)) in b.px.iter().zip(want).enumerate() {
|
||||
let got = (p[0] * 255.0).round() as i32;
|
||||
assert!((got - i32::from(w)).abs() <= 1, "b {brightness} c {contrast} at {}: {got} vs Photoshop {w}", X[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_lookup_with_an_overflowing_stored_size_is_the_identity() {
|
||||
// A stored document's size is untrusted: (2²²)³ × 3 overflows, which once wrapped past
|
||||
|
|
@ -962,3 +992,7 @@ mod tone_tests {
|
|||
assert!(a.px.iter().zip(&b.px).any(|(x, y)| x[0] != y[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy Brightness/Contrast scales contrast around 128 of 255 (contrast 80 keeps 128 at 128,
|
||||
/// measured on Photoshop 27.11; a 0.5 pivot moved it to 130).
|
||||
pub const LEGACY_PIVOT: f32 = 128.0 / 255.0;
|
||||
|
|
|
|||
|
|
@ -552,7 +552,7 @@ fn adjust(c: vec3<f32>) -> vec3<f32> {
|
|||
return vec3(select(0.0, 1.0, round_half_up(gray(c) * 255.0) >= p0.x));
|
||||
}
|
||||
case 3: { return vec3(posterize(c.r, p0.x), posterize(c.g, p0.x), posterize(c.b, p0.x)); }
|
||||
case 4: { return clamp((c - 0.5) * p0.y + 0.5 + p0.x, vec3(0.0), vec3(1.0)); } // B/C legacy
|
||||
case 4: { return clamp((c + p0.x - p0.z) * p0.y + p0.z + p0.w, vec3(0.0), vec3(1.0)); } // B/C legacy: brightness before (p0.x) or after (p0.w) contrast around compose::adjust::LEGACY_PIVOT (p0.z)
|
||||
case 5: { // B/C modern
|
||||
let b = p0.x; let ct = p0.y;
|
||||
return vec3(mcontrast(mbright(c.r, b), ct), mcontrast(mbright(c.g, b), ct), mcontrast(mbright(c.b, b), ct));
|
||||
|
|
|
|||
|
|
@ -1510,7 +1510,9 @@ pub fn adjustment_program(adj: &Adjustment, transfer: Transfer, depth: photocraf
|
|||
Adjustment::BrightnessContrast { brightness, contrast, legacy: true } => {
|
||||
let c = contrast.clamp(-100.0, 99.0);
|
||||
let k = if c >= 0.0 { 1.0 / (1.0 - c / 100.0) } else { 1.0 + c / 100.0 };
|
||||
p[0] = [brightness / 255.0, k, 0.0, 0.0];
|
||||
// Brightness before contrast when it is raised, after it when lowered (compose::adjust).
|
||||
let (pre, post) = if c >= 0.0 { (brightness / 255.0, 0.0) } else { (0.0, brightness / 255.0) };
|
||||
p[0] = [pre, k, photocraft_compose::adjust::LEGACY_PIVOT, post];
|
||||
(4, p, None)
|
||||
}
|
||||
Adjustment::BrightnessContrast { brightness, contrast, .. } => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue