2026-07-09 02:59:53 +03:00
|
|
|
#![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns, unused_assignments, clippy::cfg)]
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_psd_import_sizes_and_offsets() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let path = std::path::Path::new("_images/_test_images/example3/Example3-mini.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
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<hcie_protocol::Layer> {
|
|
|
|
|
layers.to_vec()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_psd_composite_against_ref() {
|
2026-07-13 20:33:39 +03:00
|
|
|
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");
|
2026-07-09 02:59:53 +03:00
|
|
|
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::<image::Rgba<u8>, _>::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::<image::Rgba<u8>, _>::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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
2026-07-13 20:33:39 +03:00
|
|
|
let ref_img = image::open("_images/_psd_stil_test/base_test_generated_2.png").unwrap().to_rgba8();
|
2026-07-09 02:59:53 +03:00
|
|
|
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() {
|
|
|
|
|
// Extract PSD merged image using ImageMagick
|
|
|
|
|
let _ = std::process::Command::new("convert")
|
|
|
|
|
.args(&[
|
2026-07-13 20:33:39 +03:00
|
|
|
"_images/_psd_stil_test/base_test_generated_2.psd[0]",
|
2026-07-09 02:59:53 +03:00
|
|
|
"/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();
|
|
|
|
|
|
2026-07-13 20:33:39 +03:00
|
|
|
let layers = hcie_psd::import_psd(std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd")).unwrap();
|
2026-07-09 02:59:53 +03:00
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
|
|
|
|
let teal = &layers[1]; // Teal Circle
|
|
|
|
|
|
|
|
|
|
// Extract alpha
|
|
|
|
|
let _alpha: Vec<u8> = 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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
2026-07-13 20:33:39 +03:00
|
|
|
let ref_img = image::open("_images/_psd_stil_test/base_test_generated_2.png").unwrap().to_rgba8();
|
2026-07-09 02:59:53 +03:00
|
|
|
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<hcie_protocol::Layer> = 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(
|
2026-07-13 20:33:39 +03:00
|
|
|
"_images/_psd_stil_test/base_test_generated_2.psd",
|
|
|
|
|
"_images/_psd_stil_test/base_test_generated_2.png",
|
2026-07-09 02:59:53 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_hc_emboss_composite() {
|
|
|
|
|
composite_and_compare(
|
2026-07-13 20:33:39 +03:00
|
|
|
"_images/_psd_stil_test/hc_emboss.psd",
|
|
|
|
|
"_images/_psd_stil_test/hc_emboss.png",
|
2026-07-09 02:59:53 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_base_generated_2_effect_isolation() {
|
2026-07-13 20:33:39 +03:00
|
|
|
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");
|
2026-07-09 02:59:53 +03:00
|
|
|
|
|
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
|
|
|
|
|
|
|
|
|
for (i, layer) in layers.iter().enumerate() {
|
|
|
|
|
let img = image::ImageBuffer::<image::Rgba<u8>, &[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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd");
|
2026-07-09 02:59:53 +03:00
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let test_dir = std::path::Path::new("_images/_psd_stil_test/test_2");
|
2026-07-09 02:59:53 +03:00
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
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");
|
2026-07-09 02:59:53 +03:00
|
|
|
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 !ref_path.exists() {
|
|
|
|
|
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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
let test_dir = std::path::Path::new("_images/_psd_stil_test/test_2");
|
2026-07-09 02:59:53 +03:00
|
|
|
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::<image::Rgba<u8>, _>::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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
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");
|
2026-07-09 02:59:53 +03:00
|
|
|
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::<image::Rgba<u8>, _>::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::<Vec<_>>());
|
|
|
|
|
let res_skip = hcie_composite::composite_layers(&cl_skip, canvas_w, canvas_h);
|
|
|
|
|
let ci_skip = image::ImageBuffer::<image::Rgba<u8>, _>::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::<image::Rgba<u8>, _>::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::<image::Rgba<u8>, _>::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::<image::Rgba<u8>, _>::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() {
|
2026-07-13 20:33:39 +03:00
|
|
|
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");
|
2026-07-09 02:59:53 +03:00
|
|
|
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::<image::Rgba<u8>, _>::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::<image::Rgba<u8>, _>::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::<image::Rgba<u8>, _>::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::<image::Rgba<u8>, _>::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<hcie_fx::LayerEffect> = l.effects.iter()
|
|
|
|
|
.map(|e| hcie_fx::protocol_to_hcie_fx_effect(e))
|
|
|
|
|
.collect();
|
|
|
|
|
let bevel_only: Vec<hcie_fx::LayerEffect> = 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::<image::Rgba<u8>, _>::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::<image::Rgba<u8>, Vec<u8>>::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 = [
|
2026-07-13 20:33:39 +03:00
|
|
|
"./test_resources/base_test_generated_2.psd",
|
|
|
|
|
"_images/_psd_stil_test/emboss.psd",
|
|
|
|
|
"_images/_test_images/example3/Example3-mini.psd",
|
2026-07-09 02:59:53 +03:00
|
|
|
];
|
|
|
|
|
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 = [
|
2026-07-13 20:33:39 +03:00
|
|
|
"./test_resources/base_test_generated_2.psd",
|
|
|
|
|
"_images/_psd_stil_test/emboss.psd",
|
|
|
|
|
"_images/_test_images/example3/Example3-mini.psd",
|
2026-07-09 02:59:53 +03:00
|
|
|
];
|
|
|
|
|
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<hcie_protocol::Layer> = 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");
|
|
|
|
|
}
|
|
|
|
|
}
|