From 7100b0d6177aece0eeb1f521cafaca9e6ba18fd4 Mon Sep 17 00:00:00 2001 From: MikeeI Date: Mon, 5 Oct 2026 07:00:19 +0200 Subject: [PATCH] fix(io): preserve HDR samples when unmatting PSD composites (#106) --- crates/io/src/lib.rs | 2 +- crates/io/src/pixels.rs | 3 ++- crates/io/tests/psd_structure.rs | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/io/src/lib.rs b/crates/io/src/lib.rs index c4cb637e..162c2872 100644 --- a/crates/io/src/lib.rs +++ b/crates/io/src/lib.rs @@ -167,7 +167,7 @@ pub fn merged_composite(file: &PsdFile) -> Result, IoError> { let mut v: [f32; 4] = std::array::from_fn(|i| f32::from(p[i]) / 255.0); if unmatte { for c in 0..3 { - v[c] = pixels::unmatte(v[c], v[3], 1.0); + v[c] = pixels::unmatte(v[c], v[3], 1.0).clamp(0.0, 1.0); } } v diff --git a/crates/io/src/pixels.rs b/crates/io/src/pixels.rs index 1a3b1a52..3f6a6381 100644 --- a/crates/io/src/pixels.rs +++ b/crates/io/src/pixels.rs @@ -139,8 +139,9 @@ pub fn matte(c: f32, a: f32, white: f32) -> f32 { } /// Inverse of [`matte`] (returns `white` where alpha is 0). +/// Keep HDR and negative samples; integer encoders own normalized-range clipping. pub fn unmatte(m: f32, a: f32, white: f32) -> f32 { - if a <= 0.0 { white } else { ((m - white * (1.0 - a)) / a).clamp(0.0, 1.0) } + if a <= 0.0 { white } else { (m - white * (1.0 - a)) / a } } /// `items.map(f)` on scoped threads (one per item; callers pass a handful of channels or diff --git a/crates/io/tests/psd_structure.rs b/crates/io/tests/psd_structure.rs index 1bf2909c..f072a06b 100644 --- a/crates/io/tests/psd_structure.rs +++ b/crates/io/tests/psd_structure.rs @@ -418,6 +418,15 @@ fn merged_composite_unmattes_photoshop_white() { let (d, _) = psd_to_document(&f); let p = d.layers[0].surface().unwrap().pixel(0, 0); assert!(p[1] < 0.01 && (p[3] - 128.0 / 255.0).abs() < 1e-6, "{p:?}"); + + // HDR and negative straight samples must survive the actual F32 PSD import boundary. + let mut hdr = testgen::merged_only(Version::Psd, ColorMode::Rgb, 32, Compression::Raw, 1, 1); + hdr.header.channels = 4; + hdr.image_data = + photocraft_psd::ImageData { compression: Compression::Raw, data: [1.5_f32, 0.25, 0.5, 0.5].into_iter().flat_map(f32::to_be_bytes).collect() }; + let imported = import("hdr.psd", &hdr.to_bytes().unwrap()).unwrap().document; + assert_eq!(imported.depth, photocraft_color::SampleType::F32); + assert_eq!(imported.layers[0].surface().unwrap().pixel(0, 0), vec![2.0, -0.5, 0.0, 0.5]); } #[test]