hqx 0.1.1

high-quality pixel-art image magnification (2x/3x/4x upscaling)
Documentation
//! Color conversion and the YUV difference test.
//!
//! The filter classifies each neighbor as close or distant in an approximate
//! YUV space. The conversion uses BT.601-style coefficients. The difference
//! test compares packed Y, U, and V fields against fixed thresholds.

/// Mask for the Y field of a packed YUV word.
const YMASK: u32 = 0x00FF_0000;
/// Mask for the U field of a packed YUV word.
const UMASK: u32 = 0x0000_FF00;
/// Mask for the V field of a packed YUV word.
const VMASK: u32 = 0x0000_00FF;
/// Y difference threshold, in place at bits 16..23.
const TR_Y: i32 = 0x0030_0000;
/// U difference threshold, in place at bits 8..15.
const TR_U: i32 = 0x0000_0700;
/// V difference threshold, in place at bits 0..7.
const TR_V: i32 = 0x0000_0006;
/// Mask that drops the alpha channel before color conversion.
const MASK_RGB: u32 = 0x00FF_FFFF;

/// Convert a 24-bit RGB value to packed YUV `(y << 16) + (u << 8) + v`.
///
/// Each channel forms as a fused multiply-add chain: start from the green
/// term, fuse the red term, then fuse the blue term. The fused operation
/// rounds once per step, which lands the low bit where a fused-multiply-add
/// build of the classic color table lands it. `mul_add` is deterministic
/// across targets, so the result is stable on any platform.
///
/// The float-to-unsigned conversion saturates. A negative chroma bracket
/// becomes 0 before the `+128` bias, so pure red yields U = 128 instead of a
/// wrapped value. The Y term is always non-negative. U and V stay within a
/// byte, so the packed adds never collide between fields.
#[inline]
#[must_use]
pub(crate) fn rgb_to_yuv(c: u32) -> u32 {
    let c = c & MASK_RGB;
    let r = f64::from((c >> 16) & 0xFF);
    let g = f64::from((c >> 8) & 0xFF);
    let b = f64::from(c & 0xFF);

    let y = b.mul_add(0.114, r.mul_add(0.299, 0.587 * g)) as u32;
    let u = (b.mul_add(0.5, r.mul_add(-0.169, -0.331 * g)) as u32).wrapping_add(128);
    let v = (b.mul_add(-0.081, r.mul_add(0.5, -0.419 * g)) as u32).wrapping_add(128);

    (y << 16).wrapping_add(u << 8).wrapping_add(v)
}

/// Test whether two packed YUV words differ beyond the per-channel thresholds.
///
/// Each field is isolated by its mask and kept in place. The difference is
/// taken as a wrapping `u32` subtraction, reinterpreted as `i32`, then passed
/// through `abs`. This reproduces a quirk in the classic filter: the masked
/// fields subtract as unsigned words, and the absolute value runs on the
/// wrapped result rather than a true field distance. The Y difference can
/// borrow into bits above the Y field, so the result is not a plain channel
/// delta. Matching this bit pattern is required for identical output.
#[inline]
#[must_use]
pub(crate) fn yuv_diff(yuv1: u32, yuv2: u32) -> bool {
    let dy = (yuv1 & YMASK).wrapping_sub(yuv2 & YMASK) as i32;
    let du = (yuv1 & UMASK).wrapping_sub(yuv2 & UMASK) as i32;
    let dv = (yuv1 & VMASK).wrapping_sub(yuv2 & VMASK) as i32;
    dy.wrapping_abs() > TR_Y || du.wrapping_abs() > TR_U || dv.wrapping_abs() > TR_V
}

/// Test whether two RGB colors differ by the YUV thresholds.
///
/// Both colors convert on the fly. The switch-arm runtime checks call this on
/// neighbor pairs to decide between alternative fills for ambiguous corners. A
/// `true` result means the two colors are distant enough to count as an edge.
#[inline]
#[must_use]
pub(crate) fn colors_differ(c1: u32, c2: u32) -> bool {
    yuv_diff(rgb_to_yuv(c1), rgb_to_yuv(c2))
}

#[cfg(test)]
mod tests {
    use super::*;

    /// FNV-1a 64-bit over every conversion result, byte by byte in
    /// little-endian order. Computed once from a known-good run and pinned here.
    const TABLE_FNV64: u64 = 0x3636_1769_3F45_5EF9;

    #[test]
    fn full_conversion_matches_checksum() {
        let mut h: u64 = 0xcbf2_9ce4_8422_2325;
        for c in 0u32..16_777_216 {
            let v = rgb_to_yuv(c);
            for s in (0..32).step_by(8) {
                h ^= u64::from((v >> s) & 0xFF);
                h = h.wrapping_mul(0x0000_0100_0000_01b3);
            }
        }
        assert_eq!(h, TABLE_FNV64, "conversion checksum mismatch");
    }

    #[test]
    fn named_colors_convert_exactly() {
        let cases = [
            (0x000000u32, 0x0000_8080u32),
            (0xFFFFFF, 0x00FF_8080),
            (0xFF0000, 0x004C_80FF),
            (0x00FF00, 0x0095_8080),
            (0x0000FF, 0x001D_FF80),
            (0x808080, 0x007F_8080),
            (0x123456, 0x002D_9680),
            (0x0E571F, 0x003A_8080),
            (0x244A27, 0x003A_8080),
        ];
        for (rgb, want) in cases {
            assert_eq!(rgb_to_yuv(rgb), want, "rgb {rgb:06X}");
        }
    }

    #[test]
    fn alpha_is_ignored_by_conversion() {
        // The high byte must not affect the result.
        for alpha in [0x00u32, 0x40, 0x80, 0xFF] {
            let c = (alpha << 24) | 0x123456;
            assert_eq!(rgb_to_yuv(c), rgb_to_yuv(0x123456));
        }
    }

    #[test]
    fn negative_chroma_saturates_to_bias() {
        // Pure red drives the U bracket negative. Saturation gives exactly 128.
        let red = rgb_to_yuv(0xFF0000);
        assert_eq!((red >> 8) & 0xFF, 0x80, "U field of pure red");
        // Pure green drives the V bracket negative. Saturation gives 128.
        let green = rgb_to_yuv(0x00FF00);
        assert_eq!(green & 0xFF, 0x80, "V field of pure green");
    }

    #[test]
    fn diff_is_false_for_equal_colors() {
        assert!(!yuv_diff(0x003A_8080, 0x003A_8080));
    }

    #[test]
    fn diff_respects_each_threshold() {
        let base = 0x0000_0000u32;
        // Y: a difference equal to the threshold is not greater, so no diff.
        // One step past it crosses.
        assert!(!yuv_diff(base, 0x0030_0000));
        assert!(yuv_diff(base, 0x0031_0000));
        // U: threshold 0x0700, one step past at 0x0800.
        assert!(!yuv_diff(base, 0x0000_0700));
        assert!(yuv_diff(base, 0x0000_0800));
        // V: threshold 0x06, one step past at 0x07.
        assert!(!yuv_diff(base, 0x0000_0006));
        assert!(yuv_diff(base, 0x0000_0007));
    }

    #[test]
    fn diff_uses_in_place_field_subtraction() {
        // Two RGB colors that map to the same YUV must report no difference
        // even though their raw 32-bit values differ. This is the lossy
        // collision case the pattern step relies on.
        let a = 0x0E571F;
        let b = 0x244A27;
        assert_eq!(rgb_to_yuv(a), rgb_to_yuv(b));
        assert!(!colors_differ(a, b));
    }
}