//! PSD structural comparison tool — compares original vs saved byte by byte at section level. fn main() { let args: Vec = std::env::args().collect(); let orig_path = args.get(1).map(|s| s.as_str()).unwrap_or("_images/_test_images/example3/Example3-mini.psd"); let saved_path = args.get(2).map(|s| s.as_str()).unwrap_or("_images/_test_images/example3/Example3-mini-hcie.psd"); let orig = std::fs::read(orig_path).expect("read orig"); let saved = std::fs::read(saved_path).expect("read saved"); println!("=== File sizes ==="); println!(" orig: {} bytes", orig.len()); println!(" saved: {} bytes", saved.len()); // Parse section boundaries for both files let o_sec = parse_sections(&orig); let s_sec = parse_sections(&saved); println!("\n=== Section Layout ==="); println!("Section | Original | Saved | Match"); println!("-------------------------|-------------------------|-------------------------|------"); compare_section("File Header (0..26)", 0, 26, 0, 26, &orig, &saved); compare_section("Color Mode Data", o_sec.color_start, o_sec.color_end, s_sec.color_start, s_sec.color_end, &orig, &saved); compare_section("Image Resources", o_sec.imgres_start, o_sec.imgres_end, s_sec.imgres_start, s_sec.imgres_end, &orig, &saved); compare_section("Layer&Mask Info", o_sec.lmi_start, o_sec.lmi_end, s_sec.lmi_start, s_sec.lmi_end, &orig, &saved); compare_section("Layer Info (inner)", o_sec.li_start, o_sec.li_end, s_sec.li_start, s_sec.li_end, &orig, &saved); compare_section("Image Data", o_sec.imgdata_start, orig.len(), s_sec.imgdata_start, saved.len(), &orig, &saved); // Detailed image data comparison println!("\n=== Image Data Section Detail ==="); let o_img = &orig[o_sec.imgdata_start..]; let s_img = &saved[s_sec.imgdata_start..]; if o_img.len() >= 2 && s_img.len() >= 2 { let o_comp = u16::from_be_bytes([o_img[0], o_img[1]]); let s_comp = u16::from_be_bytes([s_img[0], s_img[1]]); println!(" Compression: orig={} saved={}", o_comp, s_comp); } // Compare header bytes println!("\n=== Header Byte Differences ==="); for i in 0..26.min(orig.len()).min(saved.len()) { if orig[i] != saved[i] { println!(" byte[{}]: orig=0x{:02X} saved=0x{:02X} ({})", i, orig[i], saved[i], header_field_name(i)); } } // Check color profile in image resources println!("\n=== Color Profile Check ==="); let o_res = &orig[o_sec.imgres_start..o_sec.imgres_end]; let s_res = &saved[s_sec.imgres_start..s_sec.imgres_end]; check_icc_profile("orig", o_res); check_icc_profile("saved", s_res); // Check if saved has correct resolution info println!("\n=== Resolution Info (0x03ED) ==="); check_resolution_info("orig", o_res); check_resolution_info("saved", s_res); // Layer and mask info structure println!("\n=== Layer & Mask Info Structure ==="); let o_lmi = &orig[o_sec.lmi_start..o_sec.lmi_end]; let s_lmi = &saved[s_sec.lmi_start..s_sec.lmi_end]; if o_lmi.len() >= 8 && s_lmi.len() >= 8 { let o_lmi_len = u32::from_be_bytes([o_lmi[0], o_lmi[1], o_lmi[2], o_lmi[3]]); let o_li_len = u32::from_be_bytes([o_lmi[4], o_lmi[5], o_lmi[6], o_lmi[7]]); let s_lmi_len = u32::from_be_bytes([s_lmi[0], s_lmi[1], s_lmi[2], s_lmi[3]]); let s_li_len = u32::from_be_bytes([s_lmi[4], s_lmi[5], s_lmi[6], s_lmi[7]]); println!(" LMI length field: orig={} saved={}", o_lmi_len, s_lmi_len); println!(" LI length field: orig={} saved={}", o_li_len, s_li_len); println!(" LMI data bytes: orig={} saved={}", o_lmi.len(), s_lmi.len()); let o_layer_count = i16::from_be_bytes([o_lmi[8], o_lmi[9]]).abs(); let s_layer_count = i16::from_be_bytes([s_lmi[8], s_lmi[9]]).abs(); println!(" Layer count: orig={} saved={}", o_layer_count, s_layer_count); } } struct Sections { color_start: usize, color_end: usize, imgres_start: usize, imgres_end: usize, lmi_start: usize, lmi_end: usize, li_start: usize, li_end: usize, imgdata_start: usize, } fn parse_sections(bytes: &[u8]) -> Sections { let mut o = 26usize; let cmd_len = u32::from_be_bytes([bytes[o], bytes[o+1], bytes[o+2], bytes[o+3]]) as usize; o += 4; let color_start = o; o += cmd_len; let color_end = o; let ir_len = u32::from_be_bytes([bytes[o], bytes[o+1], bytes[o+2], bytes[o+3]]) as usize; o += 4; let imgres_start = o; o += ir_len; let imgres_end = o; let lmi_len = u32::from_be_bytes([bytes[o], bytes[o+1], bytes[o+2], bytes[o+3]]) as usize; o += 4; let lmi_start = o; o += lmi_len; let lmi_end = o; let li_len = u32::from_be_bytes([bytes[lmi_start], bytes[lmi_start+1], bytes[lmi_start+2], bytes[lmi_start+3]]) as usize; let li_start = lmi_start + 4; let li_end = li_start + li_len; Sections { color_start, color_end, imgres_start, imgres_end, lmi_start, lmi_end, li_start, li_end, imgdata_start: lmi_end, } } fn compare_section(name: &str, o_start: usize, o_end: usize, s_start: usize, s_end: usize, orig: &[u8], saved: &[u8]) { let o_data = &orig[o_start..o_end]; let s_data = &saved[s_start..s_end]; let len_match = o_data.len() == s_data.len(); let content_match = o_data == s_data; let match_str = if content_match { "YES".to_string() } else if len_match { format!("len OK, content diff ({} bytes)", o_data.len()) } else { format!("orig={} saved={}", o_data.len(), s_data.len()) }; println!(" {:<24}| {}..{} ({}) | {}..{} ({}) | {}", name, o_start, o_end, o_data.len(), s_start, s_end, s_data.len(), match_str); } fn header_field_name(i: usize) -> &'static str { match i { 0..=3 => "signature", 4..=5 => "version", 6..=11 => "reserved", 12..=13 => "channels", 14..=17 => "height", 18..=21 => "width", 22..=23 => "depth", 24..=25 => "color_mode", _ => "?", } } fn check_icc_profile(label: &str, resources: &[u8]) { let mut pos = 0; while pos + 12 <= resources.len() { if &resources[pos..pos+4] != b"8BIM" { break; } let id = u16::from_be_bytes([resources[pos+4], resources[pos+5]]); let name_len = resources[pos+6] as usize; let name_end = pos + 7 + name_len; let pad = if (name_len + 1) % 2 == 1 { 1 } else { 0 }; let data_len_pos = name_end + pad; let data_len = u32::from_be_bytes([resources[data_len_pos], resources[data_len_pos+1], resources[data_len_pos+2], resources[data_len_pos+3]]) as usize; if id == 0x040F { println!(" {}: ICC Profile found (id=0x040F, data_len={})", label, data_len); return; } let total = 6 + 1 + name_len + pad + 4 + data_len + (data_len % 2); pos += total; } println!(" {}: No ICC profile (0x040F) found!", label); } fn check_resolution_info(label: &str, resources: &[u8]) { let mut pos = 0; while pos + 12 <= resources.len() { if &resources[pos..pos+4] != b"8BIM" { break; } let id = u16::from_be_bytes([resources[pos+4], resources[pos+5]]); let name_len = resources[pos+6] as usize; let name_end = pos + 7 + name_len; let pad = if (name_len + 1) % 2 == 1 { 1 } else { 0 }; let data_len_pos = name_end + pad; let data_len = u32::from_be_bytes([resources[data_len_pos], resources[data_len_pos+1], resources[data_len_pos+2], resources[data_len_pos+3]]) as usize; if id == 0x03ED { let data_start = data_len_pos + 4; if data_len >= 16 { let hres = u32::from_be_bytes([resources[data_start], resources[data_start+1], resources[data_start+2], resources[data_start+3]]); let hres_unit = u16::from_be_bytes([resources[data_start+4], resources[data_start+5]]); let width_unit = u16::from_be_bytes([resources[data_start+6], resources[data_start+7]]); let vres = u32::from_be_bytes([resources[data_start+8], resources[data_start+9], resources[data_start+10], resources[data_start+11]]); let vres_unit = u16::from_be_bytes([resources[data_start+12], resources[data_start+13]]); let height_unit = u16::from_be_bytes([resources[data_start+14], resources[data_start+15]]); println!(" {}: hRes=0x{:08X} ({:.1} PPI) hResUnit={} widthUnit={} vRes=0x{:08X} ({:.1} PPI) vResUnit={} heightUnit={}", label, hres, (hres as f64) / 65536.0, hres_unit, width_unit, vres, (vres as f64) / 65536.0, vres_unit, height_unit); return; } } let total = 6 + 1 + name_len + pad + 4 + data_len + (data_len % 2); pos += total; } println!(" {}: No resolution info (0x03ED) found", label); }