Files

237 lines
9.9 KiB
Rust
Raw Permalink Normal View History

2026-07-09 02:59:53 +03:00
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(" ")
}