#![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns, unused_assignments, clippy::cfg)] /// Reports whether a required test fixture is absent. /// /// **Purpose:** Lets fixture-dependent integration tests remain runnable in checkouts that do not /// contain the external PSD reference corpus. /// **Logic & Workflow:** Reads filesystem metadata and classifies only `NotFound` as an optional /// fixture skip. Any other metadata error remains a hard failure so permission and filesystem /// problems are not hidden. /// **Arguments:** `path` is the required fixture path to inspect. /// **Returns:** `true` when the fixture does not exist and the caller should return early; /// otherwise `false`. /// **Side Effects / Dependencies:** Writes one diagnostic line for a missing fixture and panics on /// unexpected filesystem metadata errors. fn fixture_is_missing(path: &std::path::Path) -> bool { match std::fs::metadata(path) { Ok(_) => false, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { eprintln!("Fixture not found, skipping test: {}", path.display()); true } Err(error) => panic!( "failed to inspect test fixture '{}': {error}", path.display() ), } } #[test] fn test_psd_import_sizes_and_offsets() { let path = std::path::Path::new("_images/_test_images/example3/Example3-mini.psd"); if !path.exists() { return; } let layers = hcie_psd::import_psd(path).unwrap(); assert!(!layers.is_empty(), "No layers imported"); // Background should be at full canvas size (after reverse it's at index 0) let bg = layers.first().unwrap(); assert_eq!(bg.name, "Background"); let mut bg_non_zero = 0usize; for chunk in bg.pixels.chunks_exact(4) { if chunk[0] > 0 || chunk[1] > 0 || chunk[2] > 0 { bg_non_zero += 1; } } println!("Background non-zero pixels: {}", bg_non_zero); assert!(bg_non_zero > 1000000, "Background too empty: {}", bg_non_zero); // Layer 3 from bottom = Skin Tone Gradient (index 2 after reverse) let stg_idx = 2usize; if stg_idx < layers.len() { let stg = &layers[stg_idx]; println!("Skin Tone Gradient: {} | {}x{}", stg.name, stg.width, stg.height); } // Blemishes Removed should have some pixels at offset (index 1 after reverse) let br = &layers[1]; assert_eq!(br.name, "Blemishes Removed"); let mut br_non_zero = 0usize; for chunk in br.pixels.chunks_exact(4) { if chunk[0] > 0 || chunk[1] > 0 || chunk[2] > 0 { br_non_zero += 1; } } println!("Blemishes Removed non-zero pixels: {}", br_non_zero); assert!(br_non_zero > 0, "Blemishes Removed has no pixels"); } fn map_to_composite_layers(layers: &[hcie_protocol::Layer]) -> Vec { layers.to_vec() } #[test] fn test_psd_composite_against_ref() { let path = std::path::Path::new("_images/_test_images/example3/Example3-mini.psd"); let ref_path = std::path::Path::new("_images/_test_images/example3/Example3-mini.png"); if !path.exists() || !ref_path.exists() { println!("Test files not found, skipping comparison."); return; } let layers = hcie_psd::import_psd(path).unwrap(); assert!(!layers.is_empty(), "No layers imported"); for (i, l) in layers.iter().enumerate() { println!("Layer [{}]: name='{}' visible={} opacity={:.2} blend={:?} effects_len={} adj={} mask={} bounds={:?} mask_default={}", i, l.name, l.visible, l.opacity, l.blend_mode, l.effects.len(), l.adjustment.is_some(), l.mask_pixels.is_some(), l.mask_bounds, l.mask_default_color); } let canvas_w = layers[0].width; let canvas_h = layers[0].height; let comp_layers = map_to_composite_layers(&layers); let composited = hcie_composite::composite_layers(&comp_layers, canvas_w, canvas_h); let ref_img = image::open(ref_path).unwrap().to_rgba8(); let composited_image = image::ImageBuffer::, _>::from_raw(canvas_w, canvas_h, composited).unwrap(); let resized_composited = if canvas_w != ref_img.width() || canvas_h != ref_img.height() { println!("Resizing composited buffer from {}x{} to {}x{}", canvas_w, canvas_h, ref_img.width(), ref_img.height()); image::imageops::resize(&composited_image, ref_img.width(), ref_img.height(), image::imageops::FilterType::Triangle) } else { composited_image }; let composited = resized_composited.into_raw(); let ref_pixels = ref_img.as_raw(); assert_eq!(composited.len(), ref_pixels.len(), "Pixel buffer length mismatch"); let mut diff_sum = 0.0; let mut max_diff = 0u8; let mut match_count = 0usize; let total_pixels = (canvas_w * canvas_h) as usize; for i in 0..total_pixels { let idx = i * 4; let c_r = composited[idx]; let c_g = composited[idx + 1]; let c_b = composited[idx + 2]; let c_a = composited[idx + 3]; let r_r = ref_pixels[idx]; let r_g = ref_pixels[idx + 1]; let r_b = ref_pixels[idx + 2]; let r_a = ref_pixels[idx + 3]; let dr = (c_r as i32 - r_r as i32).abs(); let dg = (c_g as i32 - r_g as i32).abs(); let db = (c_b as i32 - r_b as i32).abs(); let da = (c_a as i32 - r_a as i32).abs(); let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > max_diff as i32 { max_diff = pix_diff as u8; } diff_sum += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { match_count += 1; } } let mean_diff = diff_sum / total_pixels as f64; let match_pct = (match_count as f64 / total_pixels as f64) * 100.0; println!("PSD Composite vs Ref Image comparison:"); println!(" Mean Absolute Error (MAE): {:.4}", mean_diff); println!(" Max Pixel Channel Difference: {}", max_diff); println!(" Pixels Matching (diff <= 5): {:.2}% ({}/{})", match_pct, match_count, total_pixels); assert!(mean_diff < 2.0, "Composite MAE too high: {:.4}", mean_diff); assert!(match_pct >= 88.0, "Too few matching pixels: {:.2}%", match_pct); } fn composite_and_compare(psd_path: &str, ref_path: &str) { let path = std::path::Path::new(psd_path); let ref_file = std::path::Path::new(ref_path); if !path.exists() || !ref_file.exists() { println!("Test files not found, skipping: {} / {}", psd_path, ref_path); return; } let layers = hcie_psd::import_psd(path).unwrap(); assert!(!layers.is_empty(), "No layers imported from {}", psd_path); let canvas_w = layers[0].width; let canvas_h = layers[0].height; println!("\n=== {} ===", psd_path); println!("Canvas: {}x{}, {} layers", canvas_w, canvas_h, layers.len()); for (i, l) in layers.iter().enumerate() { println!(" [{}] '{}' {}x{} blend={:?} opacity={:.2} vis={} clip={} effects={} fill={:.2} adj={}", i, l.name, l.width, l.height, l.blend_mode, l.opacity, l.visible, l.clipping_mask, l.effects.len(), l.fill_opacity, l.adjustment.is_some()); if let Some(bounds) = l.mask_bounds { println!(" mask_bounds={:?} mask_pixels={}", bounds, l.mask_pixels.is_some()); } for (ei, fx) in l.effects.iter().enumerate() { println!(" effect[{}]: {:?}", ei, fx); } // Print layer pixel info for debugging let mut non_zero = 0usize; let mut fully_opaque = 0usize; let mut white_pixels = 0usize; for chunk in l.pixels.chunks_exact(4) { if chunk[3] > 0 { non_zero += 1; } if chunk[3] == 255 { fully_opaque += 1; } if chunk[0] == 255 && chunk[1] == 255 && chunk[2] == 255 && chunk[3] > 0 { white_pixels += 1; } } println!(" pixels: non_zero={} fully_opaque={} white={}", non_zero, fully_opaque, white_pixels); // Show some edge pixel values let edges = [(0,0), (255,0), (0,255), (255,255), (100,40), (110,30), (236,39), (222,137)]; for (x, y) in edges { let idx = ((y * l.width + x) * 4) as usize; if idx + 3 < l.pixels.len() { println!(" pixel({},{}): RGBA=[{},{},{},{}]", x, y, l.pixels[idx], l.pixels[idx+1], l.pixels[idx+2], l.pixels[idx+3]); } } } let comp_layers = map_to_composite_layers(&layers); let composited = hcie_composite::composite_layers(&comp_layers, canvas_w, canvas_h); let ref_img = image::open(ref_file).unwrap().to_rgba8(); let composited_image = image::ImageBuffer::, _>::from_raw(canvas_w, canvas_h, composited).unwrap(); let resized_composited = if canvas_w != ref_img.width() || canvas_h != ref_img.height() { println!("Resizing composited from {}x{} to {}x{}", canvas_w, canvas_h, ref_img.width(), ref_img.height()); image::imageops::resize(&composited_image, ref_img.width(), ref_img.height(), image::imageops::FilterType::Triangle) } else { composited_image }; resized_composited.save("/tmp/hcie_composite.png").ok(); let composited_raw = resized_composited.into_raw(); let ref_pixels = ref_img.as_raw(); assert_eq!(composited_raw.len(), ref_pixels.len(), "Pixel buffer length mismatch: {} vs {}", composited_raw.len(), ref_pixels.len()); let total_pixels = ref_img.width() * ref_img.height(); let mut diff_sum = 0.0f64; let mut max_diff = 0u32; let mut match_count = 0usize; let mut diff_counts = [0u32; 256]; for i in 0..total_pixels as usize { let idx = i * 4; let dr = (composited_raw[idx] as i32 - ref_pixels[idx] as i32).unsigned_abs(); let dg = (composited_raw[idx+1] as i32 - ref_pixels[idx+1] as i32).unsigned_abs(); let db = (composited_raw[idx+2] as i32 - ref_pixels[idx+2] as i32).unsigned_abs(); let da = (composited_raw[idx+3] as i32 - ref_pixels[idx+3] as i32).unsigned_abs(); let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > max_diff { max_diff = pix_diff; } diff_sum += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { match_count += 1; } diff_counts[pix_diff.min(255) as usize] += 1; } let mean_diff = diff_sum / total_pixels as f64; let match_pct = match_count as f64 / total_pixels as f64 * 100.0; println!(" MAE: {:.4} MaxDiff: {} Match(<=5): {:.2}% ({}/{})", mean_diff, max_diff, match_pct, match_count, total_pixels); println!(" Diff distribution:"); for (d, &count) in diff_counts.iter().enumerate() { if count > 0 && d <= 20 { println!(" diff={}: {} pixels", d, count); } } let mut diff_img = image::RgbaImage::new(ref_img.width(), ref_img.height()); for i in 0..total_pixels as usize { let idx = i * 4; let dr = (composited_raw[idx] as i32 - ref_pixels[idx] as i32).unsigned_abs(); let dg = (composited_raw[idx+1] as i32 - ref_pixels[idx+1] as i32).unsigned_abs(); let db = (composited_raw[idx+2] as i32 - ref_pixels[idx+2] as i32).unsigned_abs(); let da = (composited_raw[idx+3] as i32 - ref_pixels[idx+3] as i32).unsigned_abs(); let d = (dr.max(dg).max(db).max(da).min(255) * 4).min(255) as u8; diff_img.put_pixel(i as u32 % ref_img.width(), i as u32 / ref_img.width(), image::Rgba([d, d, d, 255])); } diff_img.save("/tmp/hcie_diff.png").ok(); println!(" Saved: /tmp/hcie_composite.png /tmp/hcie_diff.png"); } #[test] fn test_layer_pixels_direct() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let ref_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.png"); if fixture_is_missing(psd_path) || fixture_is_missing(ref_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let ref_img = image::open(ref_path).unwrap().to_rgba8(); let ref_pixels = ref_img.as_raw(); // Compare layer 0 (Background Grid) pixels directly with reference let bg = &layers[0]; let mut diff_sum = 0.0; let mut match_count = 0usize; let mut max_diff = 0u8; for i in 0..(bg.width * bg.height) as usize { let idx = i * 4; let dr = (bg.pixels[idx] as i32 - ref_pixels[idx] as i32).abs(); let dg = (bg.pixels[idx+1] as i32 - ref_pixels[idx+1] as i32).abs(); let db = (bg.pixels[idx+2] as i32 - ref_pixels[idx+2] as i32).abs(); let da = (bg.pixels[idx+3] as i32 - ref_pixels[idx+3] as i32).abs(); let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > max_diff as i32 { max_diff = pix_diff as u8; } diff_sum += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { match_count += 1; } } let total_pixels = (bg.width * bg.height) as f64; let mae = diff_sum / total_pixels; let match_pct = (match_count as f64 / total_pixels) * 100.0; println!("Layer 0 (Background Grid) vs Ref: MAE={:.4} Match={:.2}% MaxDiff={}", mae, match_pct, max_diff); println!(" Sample pixels:"); for i in [0, 100, 200, 300, 400] { let idx = i * 4; println!(" ref({}): [{},{},{},{}] layer({}): [{},{},{},{}]", i, ref_pixels[idx], ref_pixels[idx+1], ref_pixels[idx+2], ref_pixels[idx+3], i, bg.pixels[idx], bg.pixels[idx+1], bg.pixels[idx+2], bg.pixels[idx+3]); } // Compare all layers' raw pixels for (li, layer) in layers.iter().enumerate() { let mut ldiff = 0.0; let mut lmatch = 0usize; let mut lmax = 0u8; for i in 0..(layer.width * layer.height) as usize { let idx = i * 4; let dr = (layer.pixels[idx] as i32 - ref_pixels[idx] as i32).abs(); let dg = (layer.pixels[idx+1] as i32 - ref_pixels[idx+1] as i32).abs(); let db = (layer.pixels[idx+2] as i32 - ref_pixels[idx+2] as i32).abs(); let da = (layer.pixels[idx+3] as i32 - ref_pixels[idx+3] as i32).abs(); let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > lmax as i32 { lmax = pix_diff as u8; } ldiff += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { lmatch += 1; } } let pixels = (layer.width * layer.height) as f64; println!("Layer {} '{}': MAE={:.4} Match={:.2}% MaxDiff={}", li, layer.name, ldiff/pixels, (lmatch as f64/pixels)*100.0, lmax); } } #[test] fn test_base_generated_2_vs_merged() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); if fixture_is_missing(psd_path) { return; } // Extract PSD merged image using ImageMagick let _ = std::process::Command::new("convert") .args(&[ "_images/_psd_stil_test/base_test_generated_2.psd[0]", "/tmp/psd2_merged_for_test.png" ]) .status(); let psd_merged = image::open("/tmp/psd2_merged_for_test.png").unwrap().to_rgba8(); let psd_pixels = psd_merged.as_raw(); let layers = hcie_psd::import_psd(psd_path).unwrap(); let canvas_w = layers[0].width; let canvas_h = layers[0].height; let comp_layers = map_to_composite_layers(&layers); let composited = hcie_composite::composite_layers(&comp_layers, canvas_w, canvas_h); let total_pixels = (canvas_w * canvas_h) as usize; let mut diff_sum = 0.0; let mut match_count = 0usize; let mut max_diff = 0u8; for j in 0..total_pixels { let idx = j * 4; let dr = (composited[idx] as i32 - psd_pixels[idx] as i32).abs(); let dg = (composited[idx+1] as i32 - psd_pixels[idx+1] as i32).abs(); let db = (composited[idx+2] as i32 - psd_pixels[idx+2] as i32).abs(); let da = (composited[idx+3] as i32 - psd_pixels[idx+3] as i32).abs(); let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > max_diff as i32 { max_diff = pix_diff as u8; } diff_sum += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { match_count += 1; } } let mae = diff_sum / total_pixels as f64; let match_pct = (match_count as f64 / total_pixels as f64) * 100.0; println!("Our Composite vs PSD Merged: MAE={:.4} Match={:.2}% MaxDiff={}", mae, match_pct, max_diff); } #[test] fn test_blend_normal() { // Test basic src-over compositing let dst = [255u8, 0, 0, 255]; // Red opaque let src = [0u8, 255, 0, 128]; // Green 50% transparent let result = hcie_blend::blend_pixels(dst, src, hcie_blend::BlendMode::Normal, 1.0); // Expected: src-over blend // sa = 128/255 = 0.502, da = 1.0 // out_a = 0.502 + 1.0 * (1 - 0.502) = 1.0 // out_r = (0.502 * 0 + 1.0 * 1.0 * (1-0.502)) / 1.0 = 0.498 = 127 // out_g = (0.502 * 1 + 1.0 * 0 * (1-0.502)) / 1.0 = 0.502 = 128 // out_b = 0 println!("blend([255,0,0,255], [0,255,0,128], Normal, 1.0) = {:?}", result); println!(" Expected approximately: [127, 128, 0, 255]"); } #[test] fn test_teal_circle_emboss_debug() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); if fixture_is_missing(psd_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let teal = &layers[1]; // Teal Circle // Extract alpha let _alpha: Vec = teal.pixels.chunks_exact(4).map(|c| c[3]).collect(); // Get the BevelEmboss effect params let fx = teal.effects.iter().find(|e| matches!(e, hcie_protocol::effects::LayerEffect::BevelEmboss { .. })).unwrap(); println!("BevelEmboss effect: {:?}", fx); } #[test] fn test_base_generated_2_no_effects() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let ref_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.png"); if fixture_is_missing(psd_path) || fixture_is_missing(ref_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let ref_img = image::open(ref_path).unwrap().to_rgba8(); let ref_pixels = ref_img.as_raw(); let canvas_w = layers[0].width; let canvas_h = layers[0].height; let total_pixels = (canvas_w * canvas_h) as usize; // Test with all effects disabled let no_effects: Vec = layers.iter().map(|l| { let mut layer = l.clone(); layer.effects.clear(); layer }).collect(); let composited = hcie_composite::composite_layers(&no_effects, canvas_w, canvas_h); let mut diff_sum = 0.0; let mut match_count = 0usize; let mut max_diff = 0u8; for j in 0..total_pixels { let idx = j * 4; let dr = (composited[idx] as i32 - ref_pixels[idx] as i32).abs(); let dg = (composited[idx+1] as i32 - ref_pixels[idx+1] as i32).abs(); let db = (composited[idx+2] as i32 - ref_pixels[idx+2] as i32).abs(); let da = (composited[idx+3] as i32 - ref_pixels[idx+3] as i32).abs(); let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > max_diff as i32 { max_diff = pix_diff as u8; } diff_sum += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { match_count += 1; } } let mae = diff_sum / total_pixels as f64; let match_pct = (match_count as f64 / total_pixels as f64) * 100.0; println!("NO EFFECTS: MAE={:.4} Match={:.2}% MaxDiff={}", mae, match_pct, max_diff); } #[test] fn test_base_generated_2_composite() { composite_and_compare( "_images/_psd_stil_test/base_test_generated_2.psd", "_images/_psd_stil_test/base_test_generated_2.png", ); } #[test] fn test_hc_emboss_composite() { composite_and_compare( "_images/_psd_stil_test/hc_emboss.psd", "_images/_psd_stil_test/hc_emboss.png", ); } #[test] fn test_base_generated_2_effect_isolation() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let ref_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.png"); if fixture_is_missing(psd_path) || fixture_is_missing(ref_path) { return; } let all_layers = hcie_psd::import_psd(psd_path).unwrap(); let ref_img = image::open(ref_path).unwrap().to_rgba8(); let ref_pixels = ref_img.as_raw(); let canvas_w = all_layers[0].width; let canvas_h = all_layers[0].height; let total_pixels = (canvas_w * canvas_h) as usize; // Baseline: all layers, all effects let baseline_layers = map_to_composite_layers(&all_layers); let baseline = hcie_composite::composite_layers(&baseline_layers, canvas_w, canvas_h); let baseline_mae = compute_mae(&baseline, ref_pixels, total_pixels); println!("Baseline (all effects): MAE={:.4}", baseline_mae); // For each layer, disable its effects and measure difference for (li, layer) in all_layers.iter().enumerate() { if layer.effects.is_empty() { continue; } let mut test_layers = baseline_layers.clone(); test_layers[li].effects.clear(); let result = hcie_composite::composite_layers(&test_layers, canvas_w, canvas_h); let test_mae = compute_mae(&result, ref_pixels, total_pixels); let improvement = baseline_mae - test_mae; println!(" Disable effects on Layer {} '{}': MAE={:.4} (delta={:+.4})", li, layer.name, test_mae, improvement); // Print effect details for (ei, fx) in layer.effects.iter().enumerate() { println!(" effect[{}]: {:?}", ei, fx); } } } fn compute_mae(a: &[u8], b: &[u8], pixels: usize) -> f64 { let mut sum = 0.0; for i in 0..pixels { let idx = i * 4; let dr = (a[idx] as i32 - b[idx] as i32).abs(); let dg = (a[idx+1] as i32 - b[idx+1] as i32).abs(); let db = (a[idx+2] as i32 - b[idx+2] as i32).abs(); let da = (a[idx+3] as i32 - b[idx+3] as i32).abs(); sum += (dr + dg + db + da) as f64 / 4.0; } sum / pixels as f64 } #[test] fn test_check_soft_orange_pixels() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); if fixture_is_missing(psd_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let orange = &layers[5]; // Soft Orange Shape println!("Soft Orange Shape layer pixels in (500-700, 500-700) region:"); for y in (500..=700).step_by(50) { for x in (500..=700).step_by(50) { let idx = ((y * orange.width + x) * 4) as usize; println!(" ({},{}): RGBA=[{},{},{},{}]", x, y, orange.pixels[idx], orange.pixels[idx+1], orange.pixels[idx+2], orange.pixels[idx+3]); } } let pink = &layers[2]; // Pink Rectangle println!("\nPink Rectangle layer pixels in (200-400, 200-400) region:"); for y in (200..=400).step_by(50) { for x in (200..=400).step_by(50) { let idx = ((y * pink.width + x) * 4) as usize; println!(" ({},{}): RGBA=[{},{},{},{}]", x, y, pink.pixels[idx], pink.pixels[idx+1], pink.pixels[idx+2], pink.pixels[idx+3]); } } } #[test] fn test_find_orange_pixels() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); if fixture_is_missing(psd_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let orange = &layers[5]; // Soft Orange Shape println!("Searching for non-zero pixels in Soft Orange Shape:"); let mut min_x = 999u32; let mut max_x = 0u32; let mut min_y = 999u32; let mut max_y = 0u32; let mut count = 0; for y in 0..orange.height { for x in 0..orange.width { let idx = ((y * orange.width + x) * 4) as usize; if orange.pixels[idx + 3] > 0 { count += 1; if x < min_x { min_x = x; } if x > max_x { max_x = x; } if y < min_y { min_y = y; } if y > max_y { max_y = y; } } } } println!(" Found {} non-zero pixels", count); println!(" Bounds: ({}, {}) to ({}, {})", min_x, min_y, max_x, max_y); // Sample some non-zero pixels println!(" Sample pixels:"); for y in (min_y..=max_y).step_by(20) { for x in (min_x..=max_x).step_by(20) { let idx = ((y * orange.width + x) * 4) as usize; if orange.pixels[idx + 3] > 0 { println!(" ({},{}): RGBA=[{},{},{},{}]", x, y, orange.pixels[idx], orange.pixels[idx+1], orange.pixels[idx+2], orange.pixels[idx+3]); } } } } #[test] fn test_layer_bounds() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); if fixture_is_missing(psd_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); for (i, layer) in layers.iter().enumerate() { let mut min_x = 999u32; let mut max_x = 0u32; let mut min_y = 999u32; let mut max_y = 0u32; let mut count = 0usize; for y in 0..layer.height { for x in 0..layer.width { let idx = ((y * layer.width + x) * 4) as usize; if layer.pixels[idx + 3] > 0 { count += 1; if x < min_x { min_x = x; } if x > max_x { max_x = x; } if y < min_y { min_y = y; } if y > max_y { max_y = y; } } } } println!("Layer {} '{}': {} non-zero pixels, bounds=({}, {}) to ({}, {})", i, layer.name, count, min_x, min_y, max_x, max_y); } } #[test] fn test_check_layer_offsets() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); if fixture_is_missing(psd_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); println!("Layer positions and sizes:"); for (i, layer) in layers.iter().enumerate() { println!(" Layer {} '{}': width={}, height={}, expected at ({}, {})", i, layer.name, layer.width, layer.height, // Find leftmost non-zero pixel { let mut min_x = 999u32; for y in 0..layer.height { for x in 0..layer.width { let idx = ((y * layer.width + x) * 4) as usize; if layer.pixels[idx + 3] > 0 && x < min_x { min_x = x; } } } min_x }, { let mut min_y = 999u32; for y in 0..layer.height { for x in 0..layer.width { let idx = ((y * layer.width + x) * 4) as usize; if layer.pixels[idx + 3] > 0 && y < min_y { min_y = y; } } } min_y } ); } } #[test] fn test_save_layer_pixels() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); if fixture_is_missing(psd_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); for (i, layer) in layers.iter().enumerate() { let img = image::ImageBuffer::, &[u8]>::from_raw( layer.width, layer.height, &layer.pixels ).unwrap(); let path = format!("/tmp/rust_layer_{}.png", i); img.save(&path).unwrap(); println!("Saved layer {} '{}' to {}", i, layer.name, path); } } #[test] fn test_pixel_layer_contributions() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); if fixture_is_missing(psd_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let test_pixels = [(300, 200), (400, 200), (200, 300), (500, 200), (500, 500)]; for (tx, ty) in test_pixels { println!("\nPixel ({}, {}):", tx, ty); for (i, layer) in layers.iter().enumerate() { let idx = ((ty * layer.width + tx) * 4) as usize; if idx + 3 < layer.pixels.len() { let a = layer.pixels[idx + 3]; if a > 0 { println!(" Layer {} '{}': RGBA=[{},{},{},{}]", i, layer.name, layer.pixels[idx], layer.pixels[idx+1], layer.pixels[idx+2], a); } } } } } #[test] fn test_load_test_2_psd() { let test_dir = std::path::Path::new("_images/_psd_stil_test/test_2"); if !test_dir.exists() { return; } let entries: Vec<_> = std::fs::read_dir(test_dir).unwrap() .filter_map(|e| e.ok()) .filter(|e| e.path().extension().map_or(false, |ext| ext == "psd")) .collect(); for entry in &entries { let path = entry.path(); let name = path.file_stem().unwrap().to_str().unwrap(); let start = std::time::Instant::now(); match hcie_psd::import_psd(&path) { Ok(layers) => { let load_time = start.elapsed(); let canvas_w = layers[0].width; let canvas_h = layers[0].height; let total_fx: usize = layers.iter().map(|l| l.effects.len()).sum(); let comp_layers = map_to_composite_layers(&layers); let comp_start = std::time::Instant::now(); let result = hcie_composite::composite_layers(&comp_layers, canvas_w, canvas_h); let comp_time = comp_start.elapsed(); let has_nonzero = result.chunks(4).any(|c| c[3] > 0); println!("OK {:40} {}x{} layers={} fx={:3} load={:?} comp={:?} nonzero={}", name, canvas_w, canvas_h, layers.len(), total_fx, load_time, comp_time, has_nonzero); } Err(e) => { println!("ERR {:40} {}", name, e); } } } } #[test] fn test_per_layer_effects_vs_photoshop_export() { let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let ref_dir = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2"); if fixture_is_missing(psd_path) { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let layer_names = [ "Background Grid", "Teal Circle", "Pink Rectangle", "Yellow Ring", "White Plus Shape", "Soft Orange Shape", ]; for (i, name) in layer_names.iter().enumerate() { if i >= layers.len() { break; } let layer = &layers[i]; let ref_path = ref_dir.join(format!("{}.png", name)); if fixture_is_missing(&ref_path) { println!("{}: export not found, skipping", name); continue; } let ref_img = image::open(&ref_path).unwrap().to_rgba8(); let ref_pixels = ref_img.as_raw(); let ref_w = ref_img.width(); let ref_h = ref_img.height(); // Apply effects to this layer let has_effects = !layer.effects.is_empty() || layer.fill_opacity < 1.0; let temp_fx: Vec<_> = layer.effects.iter().map(|e| hcie_fx::protocol_to_hcie_fx_effect(e)).collect(); let processed = if has_effects { hcie_fx::apply_layer_effects( &layer.pixels, layer.width, layer.height, &temp_fx, layer.fill_opacity, ) } else { layer.pixels.clone() }; // Crop to canvas size for comparison let crop_w = layer.width.min(ref_w); let crop_h = layer.height.min(ref_h); let mut total_pixels = 0usize; let mut diff_sum = 0.0f64; let mut match_count = 0usize; let mut max_diff = 0u32; let mut rgb_only_sum = 0.0f64; let mut alpha_only_sum = 0.0f64; let mut both_count = 0usize; let mut ref_alpha_nonzero = 0usize; let mut our_alpha_nonzero = 0usize; for y in 0..crop_h { for x in 0..crop_w { let pi = ((y * layer.width + x) * 4) as usize; let ri = ((y * ref_w + x) * 4) as usize; if ri + 3 >= ref_pixels.len() || pi + 3 >= processed.len() { continue; } total_pixels += 1; let c_r = processed[pi]; let c_g = processed[pi + 1]; let c_b = processed[pi + 2]; let c_a = processed[pi + 3]; let r_r = ref_pixels[ri]; let r_g = ref_pixels[ri + 1]; let r_b = ref_pixels[ri + 2]; let r_a = ref_pixels[ri + 3]; if r_a > 0 { ref_alpha_nonzero += 1; } if c_a > 0 { our_alpha_nonzero += 1; } let dr = (c_r as i32 - r_r as i32).unsigned_abs(); let dg = (c_g as i32 - r_g as i32).unsigned_abs(); let db = (c_b as i32 - r_b as i32).unsigned_abs(); let da = (c_a as i32 - r_a as i32).unsigned_abs(); let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > max_diff { max_diff = pix_diff; } diff_sum += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { match_count += 1; } if c_a > 0 && r_a > 0 { both_count += 1; rgb_only_sum += ((dr + dg + db) as f64) / 3.0; } if c_a == 0 && r_a > 0 { alpha_only_sum += 1.0; } } } let mae = if total_pixels > 0 { diff_sum / total_pixels as f64 } else { 0.0 }; let match_pct = if total_pixels > 0 { match_count as f64 / total_pixels as f64 * 100.0 } else { 0.0 }; let rgb_mae = if both_count > 0 { rgb_only_sum / both_count as f64 } else { 0.0 }; println!("{}: ref={}x{} our={}x{} effects={}", name, ref_w, ref_h, layer.width, layer.height, layer.effects.len()); println!(" alpha: ref={} ours={} delta={}", ref_alpha_nonzero, our_alpha_nonzero, our_alpha_nonzero as i64 - ref_alpha_nonzero as i64); println!(" MAE={:.4} Match(<=5)={:.1}% MaxDiff={} RGB_MAE(shared)={:.2}", mae, match_pct, max_diff, rgb_mae); } } #[test] fn test_test2_effects_mae() { let test_dir = std::path::Path::new("_images/_psd_stil_test/test_2"); if !test_dir.exists() { return; } let entries: Vec<_> = std::fs::read_dir(test_dir).unwrap() .filter_map(|e| e.ok()) .filter(|e| e.path().extension().map_or(false, |ext| ext == "psd")) .collect(); let mut total_mae = 0.0f64; let mut count = 0usize; for entry in &entries { let psd_path = entry.path(); let name = psd_path.file_stem().unwrap().to_str().unwrap(); let ref_path = test_dir.join(format!("{}.jpg", name)); if !ref_path.exists() { continue; } let layers = match hcie_psd::import_psd(&psd_path) { Ok(l) => l, Err(e) => { println!("ERR {}: {}", name, e); continue; } }; let ref_img = image::open(&ref_path).unwrap().to_rgba8(); let ref_w = ref_img.width(); let ref_h = ref_img.height(); let ref_pixels = ref_img.as_raw(); let canvas_w = layers[0].width; let canvas_h = layers[0].height; let comp_layers = map_to_composite_layers(&layers); let result = hcie_composite::composite_layers(&comp_layers, canvas_w, canvas_h); let comp_img = image::ImageBuffer::, _>::from_raw(canvas_w, canvas_h, result).unwrap(); let resized = image::imageops::resize(&comp_img, ref_w, ref_h, image::imageops::FilterType::Triangle); let our_pixels = resized.into_raw(); let total_pixels = (ref_w * ref_h) as usize; let mut diff_sum = 0.0f64; for i in 0..total_pixels { let idx = (i * 4) as usize; if idx + 3 >= our_pixels.len() || idx + 3 >= ref_pixels.len() { continue; } let dr = (our_pixels[idx] as i32 - ref_pixels[idx] as i32).abs(); let dg = (our_pixels[idx + 1] as i32 - ref_pixels[idx + 1] as i32).abs(); let db = (our_pixels[idx + 2] as i32 - ref_pixels[idx + 2] as i32).abs(); let da = (our_pixels[idx + 3] as i32 - ref_pixels[idx + 3] as i32).abs(); diff_sum += (dr + dg + db + da) as f64 / 4.0; } let mae = diff_sum / total_pixels as f64; total_mae += mae; count += 1; println!("{:40} MAE={:.4}", name, mae); } if count > 0 { let avg = total_mae / count as f64; println!("\nAverage MAE across {} files: {:.4}", count, avg); } } #[test] fn test_sultan_effects_mae() { let psd_path = std::path::Path::new("_images/_psd_stil_test/sultan.psd"); let ref_png = std::path::Path::new("_images/_psd_stil_test/sultan.png"); let layer_dir = std::path::Path::new("_images/_psd_stil_test/sultan"); if !psd_path.exists() || !ref_png.exists() { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let canvas_w = layers[0].width; let canvas_h = layers[0].height; println!("sultan.psd: {}x{} {} layers", canvas_w, canvas_h, layers.len()); for (i, l) in layers.iter().enumerate() { println!("[{}] '{}' {}x{} blend={:?} opacity={:.2} vis={} effects={} fill={:.2}", i, l.name, l.width, l.height, l.blend_mode, l.opacity, l.visible, l.effects.len(), l.fill_opacity); for fx in &l.effects { println!(" effect: {:?}", fx); } } // Composite full PSD and compare against sultan.png let comp_layers = map_to_composite_layers(&layers); let result = hcie_composite::composite_layers(&comp_layers, canvas_w, canvas_h); let ref_img = image::open(ref_png).unwrap().to_rgba8(); let ref_w = ref_img.width(); let ref_h = ref_img.height(); let ref_pixels = ref_img.as_raw(); let comp_img = image::ImageBuffer::, _>::from_raw(canvas_w, canvas_h, result.clone()).unwrap(); let resized = if canvas_w != ref_w || canvas_h != ref_h { image::imageops::resize(&comp_img, ref_w, ref_h, image::imageops::FilterType::Triangle) } else { comp_img }; let our_pixels = resized.into_raw(); let total_pixels = (ref_w * ref_h) as usize; let mut diff_sum = 0.0f64; let mut match_count = 0usize; let mut max_diff = 0u32; for i in 0..total_pixels { let idx = (i * 4) as usize; if idx + 3 >= our_pixels.len() || idx + 3 >= ref_pixels.len() { continue; } let dr = (our_pixels[idx] as i32 - ref_pixels[idx] as i32).unsigned_abs(); let dg = (our_pixels[idx + 1] as i32 - ref_pixels[idx + 1] as i32).unsigned_abs(); let db = (our_pixels[idx + 2] as i32 - ref_pixels[idx + 2] as i32).unsigned_abs(); let da = (our_pixels[idx + 3] as i32 - ref_pixels[idx + 3] as i32).unsigned_abs(); let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > max_diff { max_diff = pix_diff; } diff_sum += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { match_count += 1; } } let mae = diff_sum / total_pixels as f64; let match_pct = match_count as f64 / total_pixels as f64 * 100.0; println!("\nFull composite: MAE={:.4} Match(<=5)={:.1}% MaxDiff={}", mae, match_pct, max_diff); // Per-layer contribution analysis for skip_i in 0..layers.len() { let cl_skip = map_to_composite_layers(&layers.iter().enumerate() .filter(|(i, _)| *i != skip_i) .map(|(_, l)| l.clone()) .collect::>()); let res_skip = hcie_composite::composite_layers(&cl_skip, canvas_w, canvas_h); let ci_skip = image::ImageBuffer::, _>::from_raw(canvas_w, canvas_h, res_skip).unwrap(); let rz_skip = if canvas_w != ref_w || canvas_h != ref_h { image::imageops::resize(&ci_skip, ref_w, ref_h, image::imageops::FilterType::Triangle) } else { ci_skip }; let op_skip = rz_skip.into_raw(); let mut ds_skip = 0.0f64; for j in 0..total_pixels { let idx = (j * 4) as usize; if idx + 3 >= op_skip.len() || idx + 3 >= ref_pixels.len() { continue; } let dr = (op_skip[idx] as i32 - ref_pixels[idx] as i32).unsigned_abs() as f64; let dg = (op_skip[idx+1] as i32 - ref_pixels[idx+1] as i32).unsigned_abs() as f64; let db = (op_skip[idx+2] as i32 - ref_pixels[idx+2] as i32).unsigned_abs() as f64; let da = (op_skip[idx+3] as i32 - ref_pixels[idx+3] as i32).unsigned_abs() as f64; ds_skip += (dr + dg + db + da) / 4.0; } let mae_skip = ds_skip / total_pixels as f64; let delta = mae - mae_skip; println!(" Skip [{}] {:20}: MAE(w/o)={:.4} delta={:+.4}", skip_i, layers[skip_i].name, mae_skip, delta); } // Save debug outputs for key problem layers let debug_dir = std::path::Path::new("/tmp/sultan_debug"); std::fs::create_dir_all(debug_dir).ok(); for (i, l) in layers.iter().enumerate() { let has_fx = !l.effects.is_empty() || l.fill_opacity < 1.0; let temp_fx: Vec<_> = l.effects.iter().map(|e| hcie_fx::protocol_to_hcie_fx_effect(e)).collect(); let processed = if has_fx { hcie_fx::apply_layer_effects(&l.pixels, l.width, l.height, &temp_fx, l.fill_opacity) } else { l.pixels.clone() }; let img = image::ImageBuffer::, _>::from_raw(l.width, l.height, processed).unwrap(); let path = debug_dir.join(format!("layer{}_{}.png", i, l.name.replace(' ', "_"))); img.save(&path).ok(); println!(" Saved debug: {:?}", path); } // Save full composite { let comp_img = image::ImageBuffer::, _>::from_raw(canvas_w, canvas_h, result.clone()).unwrap(); comp_img.save(debug_dir.join("full_composite.png")).ok(); } // Save per-region MAE breakdown { let grid = 8; let gw = (ref_w as usize + grid - 1) / grid; let gh = (ref_h as usize + grid - 1) / grid; let mut region_mae = vec![0.0f64; gw * gh]; let mut region_count = vec![0usize; gw * gh]; for j in 0..total_pixels { let idx = (j * 4) as usize; if idx + 3 >= our_pixels.len() || idx + 3 >= ref_pixels.len() { continue; } let rx = (j as usize % ref_w as usize) / grid; let ry = (j as usize / ref_w as usize) / grid; let ri = ry * gw + rx; if ri >= region_mae.len() { continue; } let dr = (our_pixels[idx] as i32 - ref_pixels[idx] as i32).unsigned_abs(); let dg = (our_pixels[idx+1] as i32 - ref_pixels[idx+1] as i32).unsigned_abs(); let db = (our_pixels[idx+2] as i32 - ref_pixels[idx+2] as i32).unsigned_abs(); let da = (our_pixels[idx+3] as i32 - ref_pixels[idx+3] as i32).unsigned_abs(); region_mae[ri] += (dr + dg + db + da) as f64 / 4.0; region_count[ri] += 1; } // Find top 5 worst regions let mut ranked: Vec<_> = region_mae.iter().zip(region_count.iter()).enumerate() .filter(|(_, (_, c))| **c > 0) .map(|(i, (m, c))| (i, *m / *c as f64, *c)) .collect(); ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); println!(" Top 5 worst regions ({}x{} grid):", grid, grid); for (idx, rmae, cnt) in ranked.iter().take(5) { let rx = idx % gw; let ry = idx / gw; println!(" region ({},{})-({},{}) MAE={:.2} pixels={}", rx * grid, ry * grid, (rx+1)*grid, (ry+1)*grid, rmae, cnt); } } // Also per-layer comparison using JPG exports from sultan/ folder if layer_dir.exists() { let entries: Vec<_> = std::fs::read_dir(layer_dir).unwrap() .filter_map(|e| e.ok()) .filter(|e| e.path().extension().map_or(false, |ext| ext == "psd")) .collect(); for entry in &entries { let psd_p = entry.path(); let lname = psd_p.file_stem().unwrap().to_str().unwrap(); let jpg_path = layer_dir.join(format!("{}.jpg", lname)); if !jpg_path.exists() { continue; } let llayers = match hcie_psd::import_psd(&psd_p) { Ok(l) => l, Err(e) => { println!("ERR {}: {}", lname, e); continue; } }; let ref_jpg = image::open(&jpg_path).unwrap().to_rgba8(); let rw = ref_jpg.width(); let rh = ref_jpg.height(); let rp = ref_jpg.as_raw(); let cw = llayers[0].width; let ch = llayers[0].height; let cl = map_to_composite_layers(&llayers); let res = hcie_composite::composite_layers(&cl, cw, ch); let ci = image::ImageBuffer::, _>::from_raw(cw, ch, res).unwrap(); let rz = if cw != rw || ch != rh { image::imageops::resize(&ci, rw, rh, image::imageops::FilterType::Triangle) } else { ci }; let op = rz.into_raw(); let tp = (rw * rh) as usize; let mut ds = 0.0f64; for j in 0..tp { let idx = (j * 4) as usize; if idx + 3 >= op.len() || idx + 3 >= rp.len() { continue; } let dr = (op[idx] as i32 - rp[idx] as i32).unsigned_abs(); let dg = (op[idx+1] as i32 - rp[idx+1] as i32).unsigned_abs(); let db = (op[idx+2] as i32 - rp[idx+2] as i32).unsigned_abs(); let da = (op[idx+3] as i32 - rp[idx+3] as i32).unsigned_abs(); ds += (dr + dg + db + da) as f64 / 4.0; } let lmae = ds / tp as f64; println!(" {:45} MAE={:.4}", lname, lmae); } } } #[test] fn test_emboss_optimize() { let psd_path = std::path::Path::new("_images/_psd_stil_test/emboss.psd"); let ref_path = std::path::Path::new("_images/_psd_stil_test/emboss.png"); if !psd_path.exists() || !ref_path.exists() { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); let canvas_w = layers[0].width; let canvas_h = layers[0].height; for (i, l) in layers.iter().enumerate() { println!("[{}] '{}' {}x{} blend={:?} opacity={:.2} vis={} effects={} fill={:.2}", i, l.name, l.width, l.height, l.blend_mode, l.opacity, l.visible, l.effects.len(), l.fill_opacity); for fx in &l.effects { println!(" effect: {:?}", fx); } } let comp_layers = map_to_composite_layers(&layers); let result = hcie_composite::composite_layers(&comp_layers, canvas_w, canvas_h); let ref_img = image::open(ref_path).unwrap().to_rgba8(); let ref_w = ref_img.width(); let ref_h = ref_img.height(); let ref_pixels = ref_img.as_raw(); let comp_img = image::ImageBuffer::, _>::from_raw(canvas_w, canvas_h, result).unwrap(); let resized = if canvas_w != ref_w || canvas_h != ref_h { image::imageops::resize(&comp_img, ref_w, ref_h, image::imageops::FilterType::Triangle) } else { comp_img }; let our_pixels = resized.into_raw(); let total_pixels = (ref_w * ref_h) as usize; let mut diff_sum = 0.0f64; let mut match_count = 0usize; let mut max_diff = 0u32; let mut channel_sum = [0.0f64; 4]; let mut opaque_diff_sum = 0.0f64; let mut opaque_count = 0usize; let mut diff_map = vec![0u8; total_pixels * 4]; for i in 0..total_pixels { let idx = (i * 4) as usize; if idx + 3 >= our_pixels.len() || idx + 3 >= ref_pixels.len() { continue; } let dr = (our_pixels[idx] as i32 - ref_pixels[idx] as i32).unsigned_abs(); let dg = (our_pixels[idx+1] as i32 - ref_pixels[idx+1] as i32).unsigned_abs(); let db = (our_pixels[idx+2] as i32 - ref_pixels[idx+2] as i32).unsigned_abs(); let da = (our_pixels[idx+3] as i32 - ref_pixels[idx+3] as i32).unsigned_abs(); channel_sum[0] += dr as f64; channel_sum[1] += dg as f64; channel_sum[2] += db as f64; channel_sum[3] += da as f64; let pix_diff = dr.max(dg).max(db).max(da); if pix_diff > max_diff { max_diff = pix_diff; } diff_sum += (dr + dg + db + da) as f64 / 4.0; if pix_diff <= 5 { match_count += 1; } let v = (pix_diff as u32).min(255) as u8; diff_map[idx] = v; diff_map[idx+1] = v; diff_map[idx+2] = v; diff_map[idx+3] = 255; let ref_a = ref_pixels[idx + 3]; let our_a = our_pixels[idx + 3]; if ref_a > 0 && our_a > 0 { opaque_diff_sum += (dr + dg + db) as f64 / 3.0; opaque_count += 1; } } let mae = diff_sum / total_pixels as f64; let match_pct = match_count as f64 / total_pixels as f64 * 100.0; let opaque_mae = if opaque_count > 0 { opaque_diff_sum / opaque_count as f64 } else { 0.0 }; println!("\nEmboss composite: MAE={:.4} Match(<=5)={:.1}% MaxDiff={}", mae, match_pct, max_diff); println!(" Opaque-only MAE: {:.4} ({} opaque pixels of {})", opaque_mae, opaque_count, total_pixels); println!(" Per-channel MAE: R={:.2} G={:.2} B={:.2} A={:.2}", channel_sum[0] / total_pixels as f64, channel_sum[1] / total_pixels as f64, channel_sum[2] / total_pixels as f64, channel_sum[3] / total_pixels as f64); // Grid-based regional analysis (8x8 tiles) let tile = 32u32; let cols = (ref_w + tile - 1) / tile; let rows = (ref_h + tile - 1) / tile; let mut worst_regions: Vec<(f64, u32, u32, u32, u32)> = Vec::new(); for ty in 0..rows { for tx in 0..cols { let x0 = tx * tile; let y0 = ty * tile; let x1 = (x0 + tile).min(ref_w); let y1 = (y0 + tile).min(ref_h); let mut region_sum = 0.0f64; let mut region_count = 0usize; for y in y0..y1 { for x in x0..x1 { let pi = (y * ref_w + x) as usize * 4; if pi + 3 >= our_pixels.len() || pi + 3 >= ref_pixels.len() { continue; } let dr = (our_pixels[pi] as i32 - ref_pixels[pi] as i32).unsigned_abs(); let dg = (our_pixels[pi+1] as i32 - ref_pixels[pi+1] as i32).unsigned_abs(); let db = (our_pixels[pi+2] as i32 - ref_pixels[pi+2] as i32).unsigned_abs(); region_sum += (dr + dg + db) as f64 / 3.0; region_count += 1; } } if region_count > 0 { let region_mae = region_sum / region_count as f64; worst_regions.push((region_mae, x0, y0, x1, y1)); } } } worst_regions.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); println!("\n Worst 20 regions (tile={}):", tile); for (i, (rmse, x0, y0, x1, y1)) in worst_regions.iter().take(20).enumerate() { println!(" {:2}: ({},{})-({},{}) MAE={:.2}", i+1, x0, y0, x1, y1, rmse); } // Sample pixels along horizontal line through center (opaque only) let cy = ref_h / 2; println!("\n Horizontal center line (y={}):", cy); for x in (0..ref_w).step_by(2) { let pi = (cy * ref_w + x) as usize * 4; if pi + 3 >= our_pixels.len() || pi + 3 >= ref_pixels.len() { continue; } let oa = our_pixels[pi+3]; let ra = ref_pixels[pi+3]; if oa == 0 && ra == 0 { continue; } let or_ = our_pixels[pi]; let og = our_pixels[pi+1]; let ob = our_pixels[pi+2]; let rr = ref_pixels[pi]; let rg = ref_pixels[pi+1]; let rb = ref_pixels[pi+2]; let diff = ((or_ as i32 - rr as i32).unsigned_abs() + (og as i32 - rg as i32).unsigned_abs() + (ob as i32 - rb as i32).unsigned_abs()) as f64 / 3.0; println!(" x={:3}: our=({},{},{},{}) ref=({},{},{},{}) diff={:.1}", x, or_, og, ob, oa, rr, rg, rb, ra, diff); } // Vertical center line (opaque only) let cx = ref_w / 2; println!("\n Vertical center line (x={}):", cx); for y in (0..ref_h).step_by(2) { let pi = (y * ref_w + cx) as usize * 4; if pi + 3 >= our_pixels.len() || pi + 3 >= ref_pixels.len() { continue; } let oa = our_pixels[pi+3]; let ra = ref_pixels[pi+3]; if oa == 0 && ra == 0 { continue; } let or_ = our_pixels[pi]; let og = our_pixels[pi+1]; let ob = our_pixels[pi+2]; let rr = ref_pixels[pi]; let rg = ref_pixels[pi+1]; let rb = ref_pixels[pi+2]; let diff = ((or_ as i32 - rr as i32).unsigned_abs() + (og as i32 - rg as i32).unsigned_abs() + (ob as i32 - rb as i32).unsigned_abs()) as f64 / 3.0; println!(" y={:3}: our=({},{},{},{}) ref=({},{},{},{}) diff={:.1}", y, or_, og, ob, oa, rr, rg, rb, ra, diff); } // Save diff heatmap let debug_dir = std::path::Path::new("/tmp/emboss_debug"); std::fs::create_dir_all(debug_dir).ok(); let diff_img = image::ImageBuffer::, _>::from_raw(ref_w, ref_h, diff_map).unwrap(); diff_img.save(debug_dir.join("diff_heatmap.png")).ok(); // Save debug output let debug_dir = std::path::Path::new("/tmp/emboss_debug"); std::fs::create_dir_all(debug_dir).ok(); let comp_only = image::ImageBuffer::, _>::from_raw(canvas_w, canvas_h, hcie_composite::composite_layers(&comp_layers, canvas_w, canvas_h)).unwrap(); comp_only.save(debug_dir.join("our_composite.png")).ok(); // Also save per-layer debug for layers with effects for (i, l) in layers.iter().enumerate() { if l.effects.is_empty() { continue; } let temp_fx: Vec<_> = l.effects.iter().map(|e| hcie_fx::protocol_to_hcie_fx_effect(e)).collect(); let processed = hcie_fx::apply_layer_effects(&l.pixels, l.width, l.height, &temp_fx, l.fill_opacity); let img = image::ImageBuffer::, _>::from_raw(l.width, l.height, processed).unwrap(); let path = debug_dir.join(format!("layer{}_{}.png", i, l.name.replace(' ', "_"))); img.save(&path).ok(); println!(" Saved: {:?}", path); // Also test with only BevelEmboss (skip other effects) let mapped_fx: Vec = l.effects.iter() .map(|e| hcie_fx::protocol_to_hcie_fx_effect(e)) .collect(); let bevel_only: Vec = mapped_fx.iter() .filter(|e| matches!(e, hcie_fx::LayerEffect::BevelEmboss { .. })) .cloned() .collect(); if !bevel_only.is_empty() { let bevel_processed = hcie_fx::apply_layer_effects(&l.pixels, l.width, l.height, &bevel_only, l.fill_opacity); let bevel_img = image::ImageBuffer::, _>::from_raw(l.width, l.height, bevel_processed).unwrap(); bevel_img.save(debug_dir.join(format!("layer{}_{}_bevel_only.png", i, l.name.replace(' ', "_")))).ok(); // Also save the raw layer pixels (before effects) let raw_img = image::ImageBuffer::, Vec>::from_raw(l.width, l.height, l.pixels.clone()).unwrap(); raw_img.save(debug_dir.join(format!("layer{}_{}_raw.png", i, l.name.replace(' ', "_")))).ok(); } } } #[test] fn test_effects_survive_ffi_boundary() { let paths = [ "./test_resources/base_test_generated_2.psd", "_images/_psd_stil_test/emboss.psd", "_images/_test_images/example3/Example3-mini.psd", ]; let mut found = false; for p in &paths { let path = std::path::Path::new(p); if path.exists() { found = true; let layers = hcie_psd::import_psd(path).unwrap(); let mut total_effects = 0; for l in &layers { total_effects += l.effects.len(); } println!("[{}] FFI boundary test: {} layers, {} effects", p, layers.len(), total_effects); assert!(total_effects > 0, "No effects parsed from PSD - check lfx2 blocks"); break; } } if !found { eprintln!("No test PSD found, skipping FFI boundary test"); } } #[test] fn test_ffi_bincode_roundtrip() { let paths = [ "./test_resources/base_test_generated_2.psd", "_images/_psd_stil_test/emboss.psd", "_images/_test_images/example3/Example3-mini.psd", ]; let mut found = false; for p in &paths { let path = std::path::Path::new(p); if path.exists() { found = true; let layers = hcie_psd::import_psd(path).unwrap(); let serialized = bincode::serialize(&layers).unwrap(); let deserialized: Vec = bincode::deserialize(&serialized).unwrap(); let mut original_effects = 0usize; let mut roundtrip_effects = 0usize; for l in &layers { original_effects += l.effects.len(); } for l in &deserialized { roundtrip_effects += l.effects.len(); } println!("[{}] Bincode roundtrip: {} original effects, {} after bincode (should be equal)", p, original_effects, roundtrip_effects); assert_eq!(original_effects, roundtrip_effects, "Effects lost during bincode serialization!"); assert!(original_effects > 0, "No effects to verify - check that effects are parsed first"); break; } } if !found { eprintln!("No test PSD found, skipping bincode roundtrip test"); } }