LR-IMP-FORMATS: transparent areas develop on white instead of black (#1212)
The decoders keep the alpha plane, but to_working converted only the colour, so transparent pixels kept the black most encoders store under them: renders, previews, exports and contact sheets showed it. The working image is now flattened onto white, in linear light like the rest of the pipeline.
This commit is contained in:
parent
25ef65c81e
commit
a4716d9e4f
|
|
@ -11,7 +11,7 @@ let fmt: Option<Format> = sniff(&bytes); // JPEG/PNG/TIFF/WebP/A
|
|||
let d: Decoded = decode(&bytes, DecodeOptions::default())?;
|
||||
// d.image: Rgb32f, linear light in the source primaries (d.space: SourceSpace)
|
||||
// d.alpha, d.bit_depth, d.float, d.orientation (EXIF value, NOT applied), d.exif/xmp/icc (raw blobs)
|
||||
let working: Rgb32f = to_working(&d); // linear Rec.2020 D65 (Bradford)
|
||||
let working: Rgb32f = to_working(&d); // linear Rec.2020 D65 (Bradford), alpha flattened onto white
|
||||
let fast = decode(&bytes, DecodeOptions::fit(2048, 2048))?; // JPEG: DCT-domain 1/2..1/8 scaling
|
||||
let t: Thumbnail = decode_thumbnail(&bytes, 256)?; // embedded EXIF/MPF preview or scaled decode
|
||||
let h: Header = read_header(&bytes)?; // stored width/height + orientation, no pixels decoded
|
||||
|
|
|
|||
|
|
@ -244,12 +244,24 @@ pub fn decode_unguarded(bytes: &[u8], format: Format, opts: &DecodeOptions) -> R
|
|||
}
|
||||
|
||||
/// Convert a decode result to linear Rec.2020 D65 (Bradford-adapted), the develop working space.
|
||||
///
|
||||
/// The working space has no alpha: transparency is flattened onto white, in linear light like the rest
|
||||
/// of the pipeline (issue #1212).
|
||||
pub fn to_working(d: &Decoded) -> Rgb32f {
|
||||
if d.space.is_working() {
|
||||
return d.image.clone();
|
||||
let mut img = if d.space.is_working() {
|
||||
d.image.clone()
|
||||
} else {
|
||||
let m = d.space.to_working().to_f32();
|
||||
d.image.map(|p| mat_apply(&m, p))
|
||||
};
|
||||
// white is (1, 1, 1) in the working space too: the conversion is white-preserving
|
||||
if let Some(a) = d.alpha.as_ref().filter(|a| a.width == img.width && a.height == img.height) {
|
||||
for (p, &av) in img.data.iter_mut().zip(&a.data) {
|
||||
let av = av.clamp(0.0, 1.0);
|
||||
*p = p.map(|c| c * av + (1.0 - av));
|
||||
}
|
||||
}
|
||||
let m = d.space.to_working().to_f32();
|
||||
d.image.map(|p| mat_apply(&m, p))
|
||||
img
|
||||
}
|
||||
|
||||
/// Where a thumbnail came from.
|
||||
|
|
|
|||
|
|
@ -140,6 +140,24 @@ fn png_metadata_and_gray_alpha() {
|
|||
assert!((a - 128.0 / 255.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// The working image has no alpha: transparency is flattened onto white, in linear light (issue #1212:
|
||||
/// transparent areas, stored black, rendered black).
|
||||
#[test]
|
||||
fn png_transparency_develops_on_white() {
|
||||
// opaque red, transparent black (as most encoders store it), half-transparent red
|
||||
let data = [200, 40, 40, 255, 0, 0, 0, 0, 200, 40, 40, 128];
|
||||
let bytes = encode_png(&EncodeImage::new(3, 1, 4, Samples::U8(&data)), &EncodeMeta::default()).unwrap();
|
||||
let d = decode(&bytes, DecodeOptions::default()).unwrap();
|
||||
assert!(d.has_alpha);
|
||||
let opaque = encode_png(&EncodeImage::new(1, 1, 3, Samples::U8(&[200, 40, 40])), &EncodeMeta::default()).unwrap();
|
||||
let red = decode(&opaque, DecodeOptions::default()).unwrap().to_working().get(0, 0);
|
||||
let w = d.to_working();
|
||||
let a = 128.0 / 255.0;
|
||||
for (got, want) in [(w.get(0, 0), red), (w.get(1, 0), [1.0; 3]), (w.get(2, 0), red.map(|c| a * c + (1.0 - a)))] {
|
||||
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-5), "{got:?} != {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn p3_icc_to_working() {
|
||||
let icc = write_named(NamedSpace::DisplayP3);
|
||||
|
|
|
|||
|
|
@ -1016,6 +1016,17 @@ mod tests {
|
|||
assert!(probe_bytes("x.jpg", b"not an image").is_err());
|
||||
}
|
||||
|
||||
/// Issue #1212: a transparent PNG develops on white (what renders, previews, exports and contact sheets
|
||||
/// start from), not on the black its transparent pixels store.
|
||||
#[test]
|
||||
fn transparent_png_develops_on_white() {
|
||||
let rgba = [200, 40, 40, 255, 0, 0, 0, 0];
|
||||
let png = lightcraft_codecs::encode_png(&EncodeImage::new(2, 1, 4, Samples::U8(&rgba)), &EncodeMeta::default()).unwrap();
|
||||
let (img, _) = load_bytes(&png, 64).unwrap();
|
||||
assert!(img.get(0, 0)[0] > 0.3 && img.get(0, 0)[1] < 0.1, "{:?}", img.get(0, 0));
|
||||
assert!(img.get(1, 0).iter().all(|&c| (c - 1.0).abs() < 1e-4), "{:?}", img.get(1, 0));
|
||||
}
|
||||
|
||||
/// Issue #548: a small preview bins the mosaic by 8; specular points that clip a single green sample
|
||||
/// of a block on a grey surface must not become green speckles (the block's green used to take the
|
||||
/// clip level, which highlight reconstruction then kept).
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue