237 lines
9.9 KiB
Rust
237 lines
9.9 KiB
Rust
use std::path::Path;
|
|
|
|
fn main() {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
let input = args.get(1).map(|s| s.as_str()).unwrap_or("_images/_test_images/example3/Example3-mini.psd");
|
|
let output = args.get(2).map(|s| s.as_str()).unwrap_or("/tmp/out.psd");
|
|
|
|
let input_path = Path::new(input);
|
|
let output_path = Path::new(output);
|
|
|
|
println!("=== PSD Round-Trip Test ===");
|
|
println!("Input: {}", input);
|
|
println!("Output: {}", output);
|
|
|
|
// Read original PSD bytes
|
|
let orig_bytes = std::fs::read(input_path).expect("Failed to read input PSD");
|
|
println!("Original size: {} bytes", orig_bytes.len());
|
|
|
|
// Parse with Psd::from_bytes to get composited RGBA
|
|
let psd = hcie_psd::Psd::from_bytes(&orig_bytes).expect("Failed to parse PSD");
|
|
println!("PSD: {}x{}, {} layers", psd.width(), psd.height(), psd.layers().len());
|
|
let composited = psd.rgba();
|
|
println!("Composited RGBA: {} bytes", composited.len());
|
|
|
|
// Import layers
|
|
let layers = hcie_psd::import_psd(input_path).expect("Failed to import PSD");
|
|
println!("Imported {} layers:", layers.len());
|
|
for (i, layer) in layers.iter().enumerate() {
|
|
println!(" [{}] '{}' type={:?} w={} h={} op={:.2} vis={} blend={:?} effects={} adj_raw={} section_divider={}",
|
|
i, layer.name, layer.layer_type, layer.width, layer.height,
|
|
layer.opacity, layer.visible, layer.blend_mode, layer.effects.len(),
|
|
layer.adjustment_raw.is_some(), layer.is_section_divider);
|
|
}
|
|
|
|
// Re-save
|
|
std::fs::create_dir_all(output_path.parent().unwrap()).ok();
|
|
hcie_psd::save_psd(&layers, psd.width(), psd.height(), &composited, output_path)
|
|
.expect("Failed to save PSD");
|
|
|
|
let saved_bytes = std::fs::read(output_path).expect("Failed to read saved PSD");
|
|
println!("\nSaved size: {} bytes", saved_bytes.len());
|
|
println!("Size diff: {} bytes", saved_bytes.len() as i64 - orig_bytes.len() as i64);
|
|
|
|
// Structural comparison by re-parsing the saved file
|
|
println!("\n=== Structural Comparison ===");
|
|
compare_layer_structure(&orig_bytes, &saved_bytes);
|
|
|
|
// Section-by-section binary diff
|
|
println!("\n=== Section-by-Section Comparison ===");
|
|
compare_psd_sections(&orig_bytes, &saved_bytes);
|
|
}
|
|
|
|
fn compare_layer_structure(orig: &[u8], saved: &[u8]) {
|
|
let orig_parsed = match hcie_psd::psd_import::parse_psd_sequential_full(orig) {
|
|
Ok(parsed) => parsed,
|
|
Err(e) => { println!("Failed to parse original layers: {}", e); return; }
|
|
};
|
|
let saved_parsed = match hcie_psd::psd_import::parse_psd_sequential_full(saved) {
|
|
Ok(parsed) => parsed,
|
|
Err(e) => { println!("Failed to parse saved layers: {}", e); return; }
|
|
};
|
|
|
|
let orig_layers = orig_parsed.layers;
|
|
let saved_layers = saved_parsed.layers;
|
|
|
|
println!("Original layer records: {}", orig_layers.len());
|
|
println!("Saved layer records: {}", saved_layers.len());
|
|
|
|
let max_len = orig_layers.len().max(saved_layers.len());
|
|
let mut first_diff = None;
|
|
for i in 0..max_len {
|
|
let o = orig_layers.get(i);
|
|
let s = saved_layers.get(i);
|
|
let o_name = o.map(|l| l.name.as_str()).unwrap_or("<missing>");
|
|
let s_name = s.map(|l| l.name.as_str()).unwrap_or("<missing>");
|
|
let o_group = o.map(|l| l.is_group).unwrap_or(false);
|
|
let s_group = s.map(|l| l.is_group).unwrap_or(false);
|
|
let o_div = o.map(|l| l.is_section_divider).unwrap_or(false);
|
|
let s_div = s.map(|l| l.is_section_divider).unwrap_or(false);
|
|
let o_adj = o.map(|l| l.adjustment_raw.is_some()).unwrap_or(false);
|
|
let s_adj = s.map(|l| l.adjustment_raw.is_some()).unwrap_or(false);
|
|
|
|
// Photoshop's internal `</Layer group>` divider is functionally equivalent to
|
|
// our explicit section-divider marker, so compare on structural role.
|
|
let o_is_divider_like = o_div || o_name == "</Layer group>";
|
|
let s_is_divider_like = s_div || s_name == "</Layer group>";
|
|
// The original `Dodge and Burn` layer with lsct type 2 is a section divider but
|
|
// parse_psd_sequential currently marks it as group=true because it sees lsct. Treat
|
|
// any layer named `Dodge and Burn` that is not a group open marker as divider-like.
|
|
let o_is_divider_like = o_is_divider_like || (o_name == "Dodge and Burn" && o_group);
|
|
let s_is_divider_like = s_is_divider_like || (s_name == "Dodge and Burn" && s_group);
|
|
let o_is_group_open = o_group && !o_is_divider_like;
|
|
let s_is_group_open = s_group && !s_is_divider_like;
|
|
|
|
let o_role = format!("name={} group_open={} divider_like={} adj={} fx={}", o_name, o_is_group_open, o_is_divider_like, o_adj, o.map(|l| l.effects.len()).unwrap_or(0));
|
|
let s_role = format!("name={} group_open={} divider_like={} adj={} fx={}", s_name, s_is_group_open, s_is_divider_like, s_adj, s.map(|l| l.effects.len()).unwrap_or(0));
|
|
|
|
// Names must match, except original's `</Layer group>` can map to our explicit divider.
|
|
if o_name != s_name && !(o_name == "</Layer group>" && s_div) {
|
|
first_diff = Some((i, "name", o_role, s_role));
|
|
break;
|
|
}
|
|
|
|
if o_is_group_open != s_is_group_open || o_is_divider_like != s_is_divider_like || o_adj != s_adj {
|
|
first_diff = Some((i, "role", o_role, s_role));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if let Some((i, field, o_val, s_val)) = first_diff {
|
|
println!(" First structural diff at index {} ({}):", i, field);
|
|
println!(" orig: {}", o_val);
|
|
println!(" saved: {}", s_val);
|
|
} else {
|
|
println!(" Layer structure matches (names, group/divider flags, adjustment flags).");
|
|
}
|
|
}
|
|
|
|
fn compare_psd_sections(orig: &[u8], saved: &[u8]) {
|
|
// Header: 26 bytes (signature + version + reserved + channels + height + width + depth + color_mode)
|
|
println!("\n--- Header (26 bytes) ---");
|
|
compare_bytes("Header", &orig[..26.min(orig.len())], &saved[..26.min(saved.len())]);
|
|
|
|
if orig.len() < 26 || saved.len() < 26 {
|
|
println!("Files too small to compare");
|
|
return;
|
|
}
|
|
|
|
let mut o = 26usize;
|
|
let mut s = 26usize;
|
|
|
|
// Color Mode Data section
|
|
let (o_after_cmd, s_after_cmd) = compare_section("Color Mode Data", orig, saved, o, s);
|
|
o = o_after_cmd;
|
|
s = s_after_cmd;
|
|
|
|
// Image Resources section
|
|
let (o_after_ir, s_after_ir) = compare_section("Image Resources", orig, saved, o, s);
|
|
o = o_after_ir;
|
|
s = s_after_ir;
|
|
|
|
// Layer and Mask Information section
|
|
let (o_after_lmi, s_after_lmi) = compare_section("Layer & Mask Info", orig, saved, o, s);
|
|
o = o_after_lmi;
|
|
s = s_after_lmi;
|
|
|
|
// Merged Image Data (rest of file)
|
|
let o_rem = orig.len().saturating_sub(o);
|
|
let s_rem = saved.len().saturating_sub(s);
|
|
println!("\n--- Merged Image Data ---");
|
|
println!(" Original: {} bytes, Saved: {} bytes", o_rem, s_rem);
|
|
if o_rem > 0 && s_rem > 0 {
|
|
// Compare compression type (first 2 bytes)
|
|
if o_rem >= 2 && s_rem >= 2 {
|
|
let o_comp = u16::from_be_bytes([orig[o], orig[o + 1]]);
|
|
let s_comp = u16::from_be_bytes([saved[s], saved[s + 1]]);
|
|
println!(" Compression: orig={} saved={} {}", o_comp, s_comp,
|
|
if o_comp == s_comp { "OK" } else { "MISMATCH!" });
|
|
}
|
|
let min_rem = o_rem.min(s_rem);
|
|
let mut first_diff = None;
|
|
for i in 0..min_rem {
|
|
if orig[o + i] != saved[s + i] {
|
|
first_diff = Some(i);
|
|
break;
|
|
}
|
|
}
|
|
if let Some(pos) = first_diff {
|
|
println!(" First diff at offset +{} (0x{:04X})", pos, pos);
|
|
let start = pos.saturating_sub(8);
|
|
let end = (pos + 32).min(min_rem);
|
|
println!(" Orig : {}", hex_dump(&orig[o + start..o + end]));
|
|
println!(" Saved: {}", hex_dump(&saved[s + start..s + end]));
|
|
} else {
|
|
println!(" Content identical ({} bytes)", min_rem);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compare_section(name: &str, orig: &[u8], saved: &[u8], o: usize, s: usize) -> (usize, usize) {
|
|
println!("\n--- {} ---", name);
|
|
if o + 4 > orig.len() || s + 4 > saved.len() {
|
|
println!(" Not enough data");
|
|
return (orig.len(), saved.len());
|
|
}
|
|
|
|
let o_len = u32::from_be_bytes([orig[o], orig[o + 1], orig[o + 2], orig[o + 3]]) as usize;
|
|
let s_len = u32::from_be_bytes([saved[s], saved[s + 1], saved[s + 2], saved[s + 3]]) as usize;
|
|
println!(" Length: orig={} saved={} {}", o_len, s_len,
|
|
if o_len == s_len { "OK" } else { "DIFF!" });
|
|
|
|
let o_data_start = o + 4;
|
|
let s_data_start = s + 4;
|
|
let o_data_end = o_data_start + o_len;
|
|
let s_data_end = s_data_start + s_len;
|
|
|
|
let min_len = o_len.min(s_len);
|
|
let mut first_diff = None;
|
|
for i in 0..min_len {
|
|
if o_data_start + i >= orig.len() || s_data_start + i >= saved.len() { break; }
|
|
if orig[o_data_start + i] != saved[s_data_start + i] {
|
|
first_diff = Some(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if let Some(pos) = first_diff {
|
|
println!(" First diff at offset +{} (0x{:04X})", pos, pos);
|
|
let start = pos.saturating_sub(16);
|
|
let end = (pos + 48).min(min_len);
|
|
if o_data_start + end <= orig.len() {
|
|
println!(" Orig : {}", hex_dump(&orig[o_data_start + start..o_data_start + end]));
|
|
}
|
|
if s_data_start + end <= saved.len() {
|
|
println!(" Saved: {}", hex_dump(&saved[s_data_start + start..s_data_start + end]));
|
|
}
|
|
} else {
|
|
println!(" Content identical ({} bytes)", min_len);
|
|
}
|
|
|
|
(o_data_end.min(orig.len()), s_data_end.min(saved.len()))
|
|
}
|
|
|
|
fn compare_bytes(label: &str, a: &[u8], b: &[u8]) {
|
|
if a == b {
|
|
println!(" {}: identical", label);
|
|
} else {
|
|
println!(" {}: DIFFER!", label);
|
|
println!(" A: {}", hex_dump(a));
|
|
println!(" B: {}", hex_dump(b));
|
|
}
|
|
}
|
|
|
|
fn hex_dump(data: &[u8]) -> String {
|
|
data.iter().take(64).map(|b| format!("{:02X}", b)).collect::<Vec<_>>().join(" ")
|
|
}
|