Files
effectcraft/crates/psd/src/tests.rs
T
Brandon ThomasandClaude Opus 5.5 eb2b734059 PSD: take the integration branch's adapted non-finite test and psd_import formatting (#1149)
The six PSD PRs merged one by one leave #1131's test calling samples()/to_rgb(), which #1139
replaced; vangel76's integration draft #1149 has the adapted version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:47:27 -04:00

493 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::write::*;
use super::*;
fn gradient(w: u32, h: u32) -> Vec<[f32; 4]> {
(0..w * h).map(|i| [(i % w) as f32 / (w - 1).max(1) as f32, (i / w) as f32 / (h - 1).max(1) as f32, 0.25, 1.0]).collect()
}
fn close(a: &[[f32; 4]], b: &[[f32; 4]], tol: f32) -> bool {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (0..4).all(|c| (x[c] - y[c]).abs() <= tol))
}
fn doc_with_layers(depth: u16, mode: ColorMode, rle: bool) -> WDoc {
let mut d = WDoc::new(40, 30);
d.depth = depth;
d.mode = mode;
d.rle = rle;
let mut bg = WLayer::pixels("Background", Rect::new(0, 0, 40, 30), gradient(40, 30));
bg.opacity = 255;
let mut red = WLayer::solid("Red box", Rect::new(5, 6, 10, 8), [1.0, 0.0, 0.0, 1.0]).with_blend(b"mul ").with_opacity(128);
red.hidden = true;
let mut masked = WLayer::solid("Masked", Rect::new(20, 10, 12, 12), [0.0, 0.5, 1.0, 1.0]).with_blend(b"scrn");
// Mask: left half opaque, right half transparent; default (outside) black.
let mrect = Rect::new(20, 10, 12, 12);
masked.mask = Some(WMask { rect: mrect, data: (0..144).map(|i| if i % 12 < 6 { 1.0 } else { 0.0 }).collect(), default_color: 0, disabled: false });
d.layers = vec![bg, red, masked];
d
}
#[test]
fn layers_round_trip_8bit_rle() {
let bytes = write(&doc_with_layers(8, ColorMode::Rgb, true));
assert!(is_psd(&bytes));
let p = Psd::parse(bytes).unwrap();
assert_eq!((p.width, p.height, p.depth, p.mode), (40, 30, 8, ColorMode::Rgb));
assert_eq!(p.layers.len(), 3);
let names: Vec<&str> = p.layers.iter().map(|l| l.name.as_str()).collect();
assert_eq!(names, ["Background", "Red box", "Masked"]);
assert_eq!(p.layers[1].blend_key(), "mul ");
assert_eq!(p.layers[1].opacity, 128);
assert!(p.layers[1].hidden && !p.layers[0].hidden);
assert_eq!(p.layers[2].blend_key(), "scrn");
assert_eq!(p.layers[1].rect, Rect::new(5, 6, 10, 8));
// Pixels (8-bit quantisation).
let bg = p.layer_pixels(0, false).unwrap();
assert!(close(&bg.data, &gradient(40, 30), 1.0 / 255.0));
// Retain layer size vs canvas.
let red = p.layer_pixels(1, false).unwrap();
assert_eq!((red.width, red.height), (10, 8));
let red_c = p.layer_pixels(1, true).unwrap();
assert_eq!((red_c.width, red_c.height), (40, 30));
assert_eq!(red_c.data[6 * 40 + 5], [1.0, 0.0, 0.0, 1.0]);
assert_eq!(red_c.data[0][3], 0.0);
// Layer mask becomes alpha.
let m = p.layer_pixels(2, false).unwrap();
assert_eq!(m.data[0][3], 1.0);
assert_eq!(m.data[11][3], 0.0);
// Merged composite.
let c = p.composite().unwrap();
assert_eq!((c.width, c.height), (40, 30));
assert!(p.merged_alpha);
}
#[test]
fn layers_round_trip_16bit_raw_and_cmyk_gray() {
for (depth, mode, rle) in
[(16, ColorMode::Rgb, false), (16, ColorMode::Rgb, true), (8, ColorMode::Cmyk, true), (16, ColorMode::Cmyk, false), (8, ColorMode::Grayscale, false)]
{
let bytes = write(&doc_with_layers(depth, mode, rle));
let p = Psd::parse(bytes).unwrap_or_else(|e| panic!("{depth} {mode:?} {rle}: {e}"));
assert_eq!(p.layers.len(), 3, "{depth} {mode:?}");
assert_eq!(p.depth, depth);
let bg = p.layer_pixels(0, true).unwrap();
let tol = if depth == 16 { 1.0 / 30000.0 } else { 1.5 / 255.0 };
let want: Vec<[f32; 4]> =
if mode == ColorMode::Grayscale { gradient(40, 30).iter().map(|p| [p[0], p[0], p[0], 1.0]).collect() } else { gradient(40, 30) };
assert!(close(&bg.data, &want, tol), "{depth} {mode:?} {rle}: {:?} vs {:?}", bg.data[41], want[41]);
let c = p.composite().unwrap();
assert_eq!(c.data.len(), 1200);
}
}
#[test]
fn groups_build_a_tree() {
let mut d = WDoc::new(16, 16);
d.layers = vec![
WLayer::solid("Bottom", Rect::new(0, 0, 16, 16), [1.0; 4]),
WLayer::group_end(),
WLayer::solid("Inner A", Rect::new(0, 0, 4, 4), [1.0, 0.0, 0.0, 1.0]),
WLayer::group_end(),
WLayer::solid("Deep", Rect::new(2, 2, 4, 4), [0.0, 1.0, 0.0, 1.0]),
WLayer::group("Nested", false, *b"norm"),
WLayer::group("Group 1", true, *b"pass"),
WLayer::solid("Top", Rect::new(8, 8, 4, 4), [0.0, 0.0, 1.0, 1.0]),
];
let p = Psd::parse(write(&d)).unwrap();
assert_eq!(p.layers[6].section, Section::OpenFolder);
assert_eq!(p.layers[6].blend_key(), "pass");
assert_eq!(p.layers[5].section, Section::ClosedFolder);
let t = p.tree();
assert_eq!(
t,
vec![
Node::Layer(7),
Node::Group { layer: 6, children: vec![Node::Group { layer: 5, children: vec![Node::Layer(4)] }, Node::Layer(2)] },
Node::Layer(0)
]
);
}
#[test]
fn text_effects_fill_and_adjustments() {
let mut d = WDoc::new(64, 32);
let fx = Descriptor::new("null").with("masterFXSwitch", DValue::Bool(true)).with(
"DrSh",
DValue::Descriptor(
Descriptor::new("DrSh")
.with("enab", DValue::Bool(true))
.with("Md ", DValue::Enum("BlnM".into(), "Mltp".into()))
.with("Clr ", DValue::Descriptor(Descriptor::rgb([0.0, 0.0, 1.0])))
.with("Opct", DValue::UnitFloat("#Prc".into(), 60.0))
.with("Dstn", DValue::UnitFloat("#Pxl".into(), 7.0)),
),
);
d.layers = vec![
WLayer::empty("Fill", vec![solid_color_block([0.0, 1.0, 0.0])]),
WLayer::empty("Title", vec![text_block("Hello\nWorld", [10.0, 20.0], "Inter-Bold", 18.0, [1.0, 0.0, 0.0], 2)]).with_block(effects_block(&fx)),
WLayer::empty("Hue", vec![hue_sat_block(30, -20, 10)]),
WLayer::empty("Lev", vec![levels_block(10, 240, 5, 250, 1.2)]),
WLayer::empty("Inv", vec![invert_block()]),
WLayer::empty("BC", vec![brightness_block(20, -10)]),
WLayer::empty("Exp", vec![exposure_block(1.5, 0.0, 1.0)]),
WLayer::solid("Vec", Rect::new(0, 0, 64, 32), [1.0; 4]).with_block(vector_mask_block(
&[vec![[[8.0, 4.0]; 3], [[40.0, 4.0]; 3], [[40.0, 20.0]; 3]]],
64,
32,
false,
)),
];
let p = Psd::parse(write(&d)).unwrap();
assert_eq!(p.layers[0].fill, Some([0.0, 1.0, 0.0]));
let fill = p.layer_pixels(0, false).unwrap();
assert_eq!((fill.width, fill.height), (64, 32));
assert_eq!(fill.data[5], [0.0, 1.0, 0.0, 1.0]);
let t = p.layers[1].text.as_ref().unwrap();
assert_eq!(t.text, "Hello\nWorld");
assert_eq!(t.transform[4..], [10.0, 20.0]);
assert_eq!(t.style.font.as_deref(), Some("Inter-Bold"));
assert_eq!(t.style.size, Some(18.0));
assert_eq!(t.style.color, Some([1.0, 0.0, 0.0]));
assert_eq!(t.style.justification, Some(2));
let e = p.layers[1].effects.as_ref().unwrap();
let ds = e.obj("DrSh").unwrap();
assert_eq!(ds.enum_value("Md "), Some("Mltp"));
assert_eq!(ds.num("Dstn"), Some(7.0));
assert_eq!(p.layers[2].adjustment, Some(Adjustment::HueSaturation { hue: 30.0, saturation: -20.0, lightness: 10.0, colorize: false }));
assert_eq!(p.layers[3].adjustment, Some(Adjustment::Levels { in_black: 10.0, in_white: 240.0, gamma: 1.2, out_black: 5.0, out_white: 250.0 }));
assert_eq!(p.layers[4].adjustment, Some(Adjustment::Invert));
assert_eq!(p.layers[5].adjustment, Some(Adjustment::BrightnessContrast { brightness: 20.0, contrast: -10.0, legacy: true }));
assert_eq!(p.layers[6].adjustment, Some(Adjustment::Exposure { exposure: 1.5, offset: 0.0, gamma: 1.0 }));
let vm = p.layers[7].vector_mask.as_ref().unwrap();
assert_eq!(vm.subpaths.len(), 1);
assert!(vm.subpaths[0].closed);
let k = &vm.subpaths[0].knots;
assert_eq!(k.len(), 3);
assert!((k[1][1][0] - 40.0).abs() < 1e-4 && (k[2][1][1] - 20.0).abs() < 1e-4);
}
#[test]
fn packbits_round_trip() {
for src in [vec![], vec![1u8], vec![5; 300], (0..=255).collect::<Vec<u8>>(), vec![1, 1, 2, 3, 3, 3, 4, 5, 5]] {
let enc = pack_bits(&src);
let mut out = vec![];
unpack_bits(&enc, src.len(), &mut out).unwrap();
assert_eq!(out, src);
}
}
#[test]
fn truncated_and_garbage_input_errors() {
let bytes = write(&doc_with_layers(8, ColorMode::Rgb, true));
for n in [0, 3, 10, 26, 40, 100, bytes.len() / 2] {
let r = Psd::parse(bytes[..n].to_vec());
if let Ok(p) = r {
let _ = p.composite();
}
}
assert!(Psd::parse(b"8BPS\0\x09garbage".to_vec()).is_err());
assert!(Psd::parse(b"PNG".to_vec()).is_err());
}
#[test]
fn smart_objects_and_embedded_files() {
let mut inner = WDoc::new(20, 10);
inner.layers = vec![WLayer::solid("Red", Rect::new(0, 0, 20, 10), [1.0, 0.0, 0.0, 1.0])];
let inner_bytes = write(&inner);
let mut d = WDoc::new(64, 48);
// The content scaled 2× and rotated 90° clockwise: top-left at (40, 4).
let quad = [[40.0, 4.0], [40.0, 44.0], [20.0, 44.0], [20.0, 4.0]];
d.layers = vec![
WLayer::solid("Back", Rect::new(0, 0, 64, 48), [0.0, 0.0, 0.0, 1.0]),
WLayer::solid("Placed", Rect::new(20, 4, 20, 40), [0.5, 0.5, 0.5, 1.0]).with_block(smart_object_block("uuid-1", quad, [20.0, 10.0])),
];
d.linked = vec![("uuid-1".into(), "inner.psd".into(), *b"8BPS", inner_bytes.clone())];
let p = Psd::parse(write(&d)).unwrap();
assert_eq!(p.linked.len(), 1);
assert_eq!(p.linked[0].name, "inner.psd");
assert_eq!(p.linked[0].file_type, "8BPS");
assert_eq!(p.linked_data("uuid-1").unwrap(), &inner_bytes[..]);
let so = p.layers[1].smart_object.clone().unwrap();
assert_eq!(so.uuid, "uuid-1");
assert_eq!(so.quad, quad);
assert_eq!(so.size, Some([20.0, 10.0]));
let (c, sx, sy, rot) = so.placement(20.0, 10.0);
assert_eq!(c, [30.0, 24.0]);
assert!((sx - 2.0).abs() < 1e-9 && (sy - 2.0).abs() < 1e-9 && (rot - 90.0).abs() < 1e-9, "{sx} {sy} {rot}");
// The embedded document parses on its own.
let e = Psd::parse(p.linked_data("uuid-1").unwrap().to_vec()).unwrap();
assert_eq!((e.width, e.height), (20, 10));
assert!(p.layers[0].smart_object.is_none());
}
/// Replace the first layer record's bounds (`top, left, bottom, right`) in a written document.
fn patch_layer_rect(bytes: &mut [u8], from: Rect, to: Rect) {
let enc = |r: Rect| [r.top, r.left, r.bottom, r.right].iter().flat_map(|v| v.to_be_bytes()).collect::<Vec<u8>>();
let (old, new) = (enc(from), enc(to));
let at = bytes.windows(16).position(|w| w == old.as_slice()).expect("layer rect in the written file");
bytes[at..at + 16].copy_from_slice(&new);
}
#[test]
fn extreme_layer_bounds_error_instead_of_overflowing() {
// A layer spanning the whole i32 range: its width overflowed i32 subtraction.
let r = Rect::new(2, 3, 4, 5);
let mut d = WDoc::new(8, 8);
d.layers = vec![WLayer::solid("L", r, [1.0, 0.0, 0.0, 1.0])];
let mut bytes = write(&d);
patch_layer_rect(&mut bytes, r, Rect { top: i32::MIN, left: i32::MIN, bottom: i32::MAX, right: i32::MAX });
let p = Psd::parse(bytes).unwrap();
assert_eq!(p.layers[0].rect.width(), u32::MAX);
assert!(p.layer_pixels(0, false).is_err());
assert!(p.layer_pixels(0, true).is_err());
}
#[test]
fn huge_layer_bounds_error_instead_of_allocating() {
// A few bytes claiming a 2-billion-pixel-square layer must not try to allocate it.
let r = Rect::new(2, 3, 4, 5);
let mut d = WDoc::new(8, 8);
d.layers = vec![WLayer::solid("L", r, [1.0, 0.0, 0.0, 1.0])];
let mut bytes = write(&d);
patch_layer_rect(&mut bytes, r, Rect { top: 0, left: 0, bottom: i32::MAX, right: i32::MAX });
let p = Psd::parse(bytes).unwrap();
assert!(matches!(p.layer_pixels(0, false), Err(Error::Unsupported(_))));
}
/// `n` groups nested in each other, each holding one pixel layer (file order: bottom first).
fn nested_groups_doc(n: usize) -> WDoc {
let mut d = WDoc::new(4, 4);
for _ in 0..n {
d.layers.push(WLayer::group_end());
}
for i in 0..n {
d.layers.push(WLayer::solid(&format!("L{i}"), Rect::new(0, 0, 1, 1), [1.0; 4]));
d.layers.push(WLayer::group(&format!("G{i}"), true, *b"norm"));
}
d
}
fn depth_of(nodes: &[Node]) -> usize {
nodes.iter().map(|n| if let Node::Group { children, .. } = n { 1 + depth_of(children) } else { 0 }).max().unwrap_or(0)
}
fn layer_indices(nodes: &[Node], out: &mut Vec<usize>) {
for n in nodes {
match n {
Node::Layer(i) => out.push(*i),
Node::Group { layer, children } => {
out.push(*layer);
layer_indices(children, out);
}
}
}
}
#[test]
fn deeply_nested_groups_are_flattened_at_the_depth_limit() {
assert_eq!(MAX_GROUP_DEPTH, 64);
let n = 70;
let p = Psd::parse(write(&nested_groups_doc(n))).unwrap();
let (t, flattened) = p.tree_report();
assert_eq!(depth_of(&t), MAX_GROUP_DEPTH);
assert_eq!(flattened, 6);
// No layer or group record is lost, each appears exactly once (dividers are not nodes).
let mut seen = vec![];
layer_indices(&t, &mut seen);
seen.sort_unstable();
assert_eq!(seen, (n..p.layers.len()).collect::<Vec<_>>());
// At the limit nothing is flattened.
let (t, flattened) = Psd::parse(write(&nested_groups_doc(MAX_GROUP_DEPTH))).unwrap().tree_report();
assert_eq!((depth_of(&t), flattened), (MAX_GROUP_DEPTH, 0));
}
#[test]
fn thousands_of_nested_groups_do_not_overflow_a_small_stack() {
let bytes = write(&nested_groups_doc(5000));
let h = std::thread::Builder::new()
.stack_size(2 << 20)
.spawn(move || {
let p = Psd::parse(bytes).unwrap();
let t = p.tree();
let c = t.clone();
assert_eq!(t, c);
let (_, flattened) = p.tree_report();
drop(t);
drop(c);
flattened
})
.unwrap();
assert_eq!(h.join().unwrap(), 5000 - MAX_GROUP_DEPTH);
}
#[test]
fn non_finite_transform_and_exposure_values_are_dropped() {
let mut d = WDoc::new(16, 16);
d.layers = vec![
WLayer::empty("Title", vec![text_block("x", [f64::NAN, f64::INFINITY], "Inter", 12.0, [0.0; 3], 0)]),
WLayer::empty("Exp", vec![exposure_block(f32::NAN, f32::INFINITY, f32::NEG_INFINITY)]),
];
let p = Psd::parse(write(&d)).unwrap();
let t = p.layers[0].text.as_ref().unwrap();
assert!(t.transform.iter().all(|v| v.is_finite()));
assert_eq!(t.transform, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
assert_eq!(p.layers[1].adjustment, Some(Adjustment::Exposure { exposure: 0.0, offset: 0.0, gamma: 1.0 }));
}
#[test]
fn non_finite_32_bit_samples_become_finite() {
let mut d = WDoc::new(4, 4);
d.depth = 32;
d.layers = vec![WLayer::solid("L", Rect::new(0, 0, 4, 4), [0.5, 0.5, 0.5, 1.0])];
let p = Psd::parse(write(&d)).unwrap();
let raw: Vec<u8> = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.25].iter().flat_map(|v| v.to_be_bytes()).collect();
let mut s = vec![f32::NAN; 4];
p.for_each_sample(&raw, 4, 1, |i, v| s[i] = v);
assert_eq!(s, vec![0.0, 1.0, 0.0, 0.25]);
assert!(p.rgb_from([s[1], s[1], s[1], 0.0]).iter().all(|v| v.is_finite()));
}
#[test]
fn rect_new_near_i32_max_saturates() {
let r = Rect::new(i32::MAX, i32::MAX, 1, i32::MAX);
assert_eq!((r.right, r.bottom), (i32::MAX, i32::MAX));
assert!(r.is_empty());
}
fn find(bytes: &[u8], needle: &[u8]) -> usize {
bytes.windows(needle.len()).position(|w| w == needle).expect("needle present")
}
/// The second layer record's signature overwritten.
fn bad_signature() -> Vec<u8> {
let mut bytes = write(&doc_with_layers(8, ColorMode::Rgb, true));
let at = find(&bytes, b"8BIMmul ");
bytes[at] = b'X';
bytes
}
/// The first channel of the first layer claims more data than the file holds.
fn bad_channel_length() -> Vec<u8> {
let mut bytes = write(&doc_with_layers(8, ColorMode::Rgb, true));
let at = find(&bytes, b"8BIMnorm");
let first_len = at - 4 * 6 + 2;
bytes[first_len..first_len + 4].copy_from_slice(&0x7fff_fff0u32.to_be_bytes());
bytes
}
#[test]
fn damaged_layer_section_keeps_the_merged_image() {
let good = Psd::parse(write(&doc_with_layers(8, ColorMode::Rgb, true))).unwrap();
assert!(good.warnings.is_empty() && good.layers.len() == 3);
let want = good.composite().unwrap();
for bytes in [bad_signature(), bad_channel_length()] {
let p = Psd::parse(bytes).unwrap();
assert!(p.layers.is_empty());
assert_eq!(p.warnings.len(), 1, "{:?}", p.warnings);
assert!(p.warnings[0].contains("Layers could not be read"));
assert_eq!((p.width, p.height), (40, 30));
let got = p.composite().unwrap();
assert_eq!(got.data, want.data);
}
}
#[test]
fn header_damage_and_missing_merged_image_still_fail() {
let good = write(&doc_with_layers(8, ColorMode::Rgb, true));
// Zero width.
let mut b = good.clone();
b[18..22].copy_from_slice(&0u32.to_be_bytes());
assert!(Psd::parse(b).is_err());
// Unknown colour mode.
let mut b = good.clone();
b[24..26].copy_from_slice(&99u16.to_be_bytes());
assert!(Psd::parse(b).is_err());
// Damaged layers and no merged image at all: nothing to fall back to.
let end = Psd::parse(good).unwrap().merged_offset;
let mut b = bad_signature();
b.truncate(end);
assert!(Psd::parse(b).is_err());
}
/// A ZIP plane body for the decoder: the plane delta-coded per row when `predict`.
fn zip_body(raw: &[u8], w: usize, depth: u16, predict: bool) -> Vec<u8> {
if !predict {
return raw.to_vec();
}
let mut out = raw.to_vec();
let bps = usize::from(depth / 8);
for row in out.chunks_exact_mut(w * bps) {
if depth == 16 {
let mut prev = 0u16;
for c in row.as_chunks_mut::<2>().0 {
let x = u16::from_be_bytes([c[0], c[1]]);
c.copy_from_slice(&x.wrapping_sub(prev).to_be_bytes());
prev = x;
}
} else {
for i in (1..row.len()).rev() {
row[i] = row[i].wrapping_sub(row[i - 1]);
}
}
}
out
}
/// The same document with every channel and the merged image re-encoded as ZIP (`comp` 2) or
/// ZIP with prediction (3), appended to the file data.
fn rezip(p: &Psd, comp: u16) -> Psd {
let mut z = p.clone();
let mut data = p.data.to_vec();
let (depth, predict) = (p.depth, comp == 3);
let deflate = |body: &[u8]| {
let mut v = comp.to_be_bytes().to_vec();
v.extend(miniz_oxide::deflate::compress_to_vec_zlib(body, 6));
v
};
for (l, zl) in p.layers.iter().zip(&mut z.layers) {
for (c, zc) in l.channels.iter().zip(&mut zl.channels) {
let w = if c.id == -2 { l.mask.map_or(0, |m| m.rect.width() as usize) } else { l.rect.width() as usize };
let raw = &p.data[c.offset + 2..c.offset + c.len];
zc.offset = data.len();
data.extend(deflate(&zip_body(raw, w, depth, predict)));
zc.len = data.len() - zc.offset;
}
}
// The merged image: one ZIP stream holding all planes.
let (w, h) = (p.width as usize, p.height as usize);
let plane = w * h * usize::from(depth / 8);
let mut all = vec![];
for c in 0..usize::from(p.channels) {
let at = p.merged_offset + 2 + c * plane;
all.extend(zip_body(&p.data[at..at + plane], w, depth, predict));
}
z.merged_offset = data.len();
data.extend(deflate(&all));
z.data = data.into();
z
}
#[test]
fn zip_channels_decode_like_raw_ones() {
for depth in [8, 16] {
let raw = Psd::parse(write(&doc_with_layers(depth, ColorMode::Rgb, false))).unwrap();
for comp in [2, 3] {
let zip = rezip(&raw, comp);
assert_eq!(zip.composite().unwrap().data, raw.composite().unwrap().data, "composite {depth} bit, compression {comp}");
for (i, canvas) in [(0, true), (0, false), (1, false), (2, false), (2, true)] {
let (want, got) = (raw.layer_pixels(i, canvas).unwrap(), zip.layer_pixels(i, canvas).unwrap());
assert_eq!(got.data, want.data, "layer {i} canvas {canvas}, {depth} bit, compression {comp}");
}
}
}
}
#[test]
fn rle_composite_matches_the_raw_one() {
for depth in [8, 16] {
let raw = Psd::parse(write(&doc_with_layers(depth, ColorMode::Rgb, false))).unwrap();
let rle = Psd::parse(write(&doc_with_layers(depth, ColorMode::Rgb, true))).unwrap();
assert_eq!(rle.composite().unwrap().data, raw.composite().unwrap().data, "{depth} bit");
assert_eq!(rle.layer_pixels(2, true).unwrap().data, raw.layer_pixels(2, true).unwrap().data, "{depth} bit");
}
}