use std::path::Path; fn main() { let args: Vec = 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(""); let s_name = s.map(|l| l.name.as_str()).unwrap_or(""); 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 `` divider is functionally equivalent to // our explicit section-divider marker, so compare on structural role. let o_is_divider_like = o_div || o_name == ""; let s_is_divider_like = s_div || s_name == ""; // 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 `` can map to our explicit divider. if o_name != s_name && !(o_name == "" && 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::>().join(" ") }