zenjpeg 0.8.0

Pure Rust JPEG encoder/decoder with perceptual optimizations
Documentation
//! XYB color space roundtrip tests.
//!
//! Tests that XYB conversion matches C++ implementation and roundtrips correctly.
//! These are internal tests that access the xyb module directly.

use super::xyb::{
    linear_to_srgb_u8, rgb_buffer_to_xyb_planes, srgb_to_xyb, srgb_u8_to_linear,
    xyb_planes_to_rgb_buffer, xyb_to_srgb,
};

/// Test XYB roundtrip for all 8-bit colors at key points.
#[test]
fn test_xyb_roundtrip_comprehensive() {
    let mut max_error = 0i16;
    let mut error_count = 0usize;
    let mut total_tests = 0usize;

    // Test key colors: corners, edges, gray ramp
    let test_values: Vec<u8> = vec![
        0, 1, 2, 4, 8, 16, 32, 64, 96, 128, 160, 192, 224, 240, 252, 254, 255,
    ];

    for &r in &test_values {
        for &g in &test_values {
            for &b in &test_values {
                let (x, y, b_xyb) = srgb_to_xyb(r, g, b);
                let (r2, g2, b2) = xyb_to_srgb(x, y, b_xyb);

                let dr = (r as i16 - r2 as i16).abs();
                let dg = (g as i16 - g2 as i16).abs();
                let db = (b as i16 - b2 as i16).abs();

                max_error = max_error.max(dr).max(dg).max(db);
                if dr > 1 || dg > 1 || db > 1 {
                    error_count += 1;
                }
                total_tests += 1;
            }
        }
    }

    println!(
        "XYB roundtrip: {} tests, max error: {}, errors>1: {}",
        total_tests, max_error, error_count
    );

    // Allow max error of 2 due to float precision
    assert!(
        max_error <= 2,
        "XYB roundtrip max error {} exceeds tolerance",
        max_error
    );

    // At most 5% of values should have error > 1
    let error_ratio = error_count as f64 / total_tests as f64;
    assert!(
        error_ratio < 0.05,
        "XYB roundtrip error ratio {} exceeds 5%",
        error_ratio
    );
}

/// Test that gray values maintain X ≈ 0.
#[test]
fn test_xyb_gray_neutrality() {
    for gray in 0..=255u8 {
        let (x, _y, _b) = srgb_to_xyb(gray, gray, gray);
        assert!(x.abs() < 0.01, "Gray {} has X={}, should be ~0", gray, x);
    }
}

/// Test XYB values for known reference colors (from C++ implementation).
#[test]
fn test_xyb_reference_values() {
    // Reference values from C++ jpegli (approximate)
    let test_cases = [
        // (r, g, b, expected_x, expected_y, expected_b) with tolerance
        (0u8, 0u8, 0u8, 0.0f32, 0.0f32, 0.0f32, 0.01), // Black
        (255u8, 255u8, 255u8, 0.0f32, 0.88f32, 0.82f32, 0.05), // White (approximate)
        (255u8, 0u8, 0u8, 0.1f32, 0.55f32, 0.08f32, 0.1), // Red
        (0u8, 255u8, 0u8, -0.15f32, 0.7f32, 0.25f32, 0.1), // Green
        (0u8, 0u8, 255u8, 0.0f32, 0.3f32, 0.75f32, 0.1), // Blue
    ];

    for (r, g, b, _exp_x, _exp_y, _exp_b, _tol) in test_cases {
        let (x, y, b_xyb) = srgb_to_xyb(r, g, b);
        println!(
            "RGB({},{},{}) -> XYB({:.4}, {:.4}, {:.4})",
            r, g, b, x, y, b_xyb
        );
        // Just log for now - exact values depend on C++ constants
    }
}

/// Test buffer conversion functions.
#[test]
fn test_xyb_buffer_roundtrip() {
    let width = 16;
    let height = 16;
    let mut rgb = vec![0u8; width * height * 3];

    // Create gradient test pattern
    for y in 0..height {
        for x in 0..width {
            let idx = (y * width + x) * 3;
            rgb[idx] = (x * 16) as u8;
            rgb[idx + 1] = (y * 16) as u8;
            rgb[idx + 2] = ((x + y) * 8) as u8;
        }
    }

    // Convert to XYB
    let (x_plane, y_plane, b_plane) = rgb_buffer_to_xyb_planes(&rgb, width, height);

    // Convert back
    let rgb2 = xyb_planes_to_rgb_buffer(&x_plane, &y_plane, &b_plane, width, height);

    // Check roundtrip
    let mut max_diff = 0i16;
    for i in 0..rgb.len() {
        let diff = (rgb[i] as i16 - rgb2[i] as i16).abs();
        max_diff = max_diff.max(diff);
    }

    println!("Buffer roundtrip max diff: {}", max_diff);
    assert!(
        max_diff <= 2,
        "Buffer roundtrip max diff {} exceeds 2",
        max_diff
    );
}

/// Test linear RGB conversion accuracy.
#[test]
fn test_srgb_linear_precision() {
    let mut max_error = 0f32;

    for v in 0..=255u8 {
        let linear = srgb_u8_to_linear(v);
        let back = linear_to_srgb_u8(linear);

        // Check value is in valid range
        assert!(
            (0.0..=1.0).contains(&linear),
            "Linear value {} out of range for input {}",
            linear,
            v
        );

        // Allow 1-bit error due to rounding
        let error = (v as i16 - back as i16).abs();
        max_error = max_error.max(error as f32);
    }

    assert!(
        max_error <= 1.0,
        "sRGB<->linear max error {} exceeds 1",
        max_error
    );
}