Address CodeRabbit review: a layered Duotone writes its real merged image, for #4935

A Duotone PSD opens flattened from its merged image (its layer records are
not read back), but with Maximize Compatibility off a layered Duotone was
saved with the white placeholder, so reopening it showed white. Its merged
image is now always written, like a document without layer records.

Test: a_layered_duotone_keeps_its_image_without_maximize_compatibility.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Mann
2026-10-21 21:04:36 +02:00
co-authored by Claude Opus 5.5
parent 45c7b57b6a
commit a1b56b2d72
2 changed files with 28 additions and 2 deletions
+3 -2
View File
@@ -1679,8 +1679,9 @@ fn document_to_psd_nested(doc: &Document, opts: &PsdExportOptions, depth: u32, c
// Without Maximize Compatibility the merged image is a white placeholder and the composite
// isn't rendered at all. A document without layer records keeps the real image: its pixels
// live nowhere else.
let real_merged = opts.maximize_compatibility || ex.records.is_empty();
// live nowhere else. So does a Duotone: it opens flattened from that image (its layer records
// are not read back).
let real_merged = opts.maximize_compatibility || ex.records.is_empty() || duotone_inks.is_some();
let (mut planes, has_alpha) = if real_merged {
// Merged composite, rendered and encoded in bands (no full-size float composite). Matting
// against white only changes pixels with alpha < 1; if some are slightly translucent but
+25
View File
@@ -245,6 +245,31 @@ fn a_16_bit_duotone_is_written_as_grayscale() {
assert!(out.warnings.iter().any(|w| w == "Duotone document written as Grayscale"), "{:?}", out.warnings);
}
#[test]
fn a_layered_duotone_keeps_its_image_without_maximize_compatibility() {
// A Duotone opens flattened from its merged image (its layer records are not read), so that
// image is written in full even when Maximize Compatibility is off, never the placeholder.
let mut d = duotone_doc(photocraft_color::SampleType::U8);
let mut top = photocraft_doc::Layer::new("Dark", LayerContent::Raster(photocraft_raster::Surface::new(d.pixel_format())));
if let LayerContent::Raster(s) = &mut top.content {
s.fill_rect(photocraft_geom::Rect::new(0, 0, 2, 2), &photocraft_raster::from_rgba(&d.pixel_format(), [0.0, 0.0, 0.0, 1.0]));
}
d.layers.push(top);
let opts = ExportOptions { psd_maximize_compatibility: false, ..Default::default() };
let f = PsdFile::from_bytes(&export(&d, "x.psd", &opts).unwrap().bytes).unwrap();
assert_eq!(f.header.color_mode, ColorMode::Duotone);
assert!(!f.layers().is_empty(), "the layers are still written");
// The grey plate: the left half black (the top layer), the right half the 50% grey background.
let plate: Vec<u8> = f.decode_merged().unwrap();
assert_eq!(plate.len(), 8);
assert!(plate[..2].iter().chain(&plate[4..6]).all(|&v| v == 0), "{plate:?}");
assert!(plate[2..4].iter().chain(&plate[6..]).all(|&v| (127..=128).contains(&v)), "{plate:?}");
let (back, _) = psd_to_document(&f);
let maxed = PsdFile::from_bytes(&export(&d, "x.psd", &ExportOptions::default()).unwrap().bytes).unwrap();
assert_eq!(f.decode_merged().unwrap(), maxed.decode_merged().unwrap());
assert_eq!(back.duotone.as_ref().map(|x| x.inks.len()), Some(2));
}
#[test]
fn a_colour_book_ink_shows_its_lab_equivalent_or_the_grey_plate() {
let out = export(&duotone_doc(photocraft_color::SampleType::U8), "x.psd", &ExportOptions::default()).unwrap();