hqx 0.1.1

high-quality pixel-art image magnification (2x/3x/4x upscaling)
Documentation
//! Weighted color blends used by the fill tables.
//!
//! Blends split each pixel into channel groups and combine them with integer
//! weights and a right shift. Alpha blends separately: it shifts to a byte,
//! scales, then shifts back by `24 - s`. Green blends alone under `MASK_2`.
//! Red and blue blend together under `MASK_13`. The three lane masks are
//! disjoint and cover the whole word, so a plain add recombines them with no
//! carry between fields. The two-color blend returns the input unchanged when
//! both colors are equal, which preserves exact bits including alpha.

/// Mask for the green byte.
const MASK_2: u32 = 0x0000_FF00;
/// Mask for the red and blue bytes together.
const MASK_13: u32 = 0x00FF_00FF;
/// Mask for the alpha byte.
const MASK_ALPHA: u32 = 0xFF00_0000;

/// Blend two colors with integer weights `w1`, `w2` and shift `s`.
///
/// Returns `c1` unchanged when `c1 == c2`. Otherwise it blends alpha, green,
/// and the red/blue pair in separate lanes. The lanes are masked into disjoint
/// fields, so a plain add recombines them with no carry between fields.
#[inline]
#[must_use]
pub(crate) fn interpolate_2(c1: u32, w1: u32, c2: u32, w2: u32, s: u32) -> u32 {
    if c1 == c2 {
        return c1;
    }
    let alpha = ((((c1 & MASK_ALPHA) >> 24) * w1 + ((c2 & MASK_ALPHA) >> 24) * w2) << (24 - s))
        & MASK_ALPHA;
    let green = (((c1 & MASK_2) * w1 + (c2 & MASK_2) * w2) >> s) & MASK_2;
    let redblue = (((c1 & MASK_13) * w1 + (c2 & MASK_13) * w2) >> s) & MASK_13;
    alpha + green + redblue
}

/// Blend three colors with integer weights and shift `s`.
///
/// The three-color blend has no early return, so equal inputs still pass
/// through the lane arithmetic. The alpha, green, and red/blue lanes combine
/// the same way as the two-color form, recombined by a plain add over the
/// disjoint masks.
#[inline]
#[must_use]
pub(crate) fn interpolate_3(c1: u32, w1: u32, c2: u32, w2: u32, c3: u32, w3: u32, s: u32) -> u32 {
    let alpha = ((((c1 & MASK_ALPHA) >> 24) * w1
        + ((c2 & MASK_ALPHA) >> 24) * w2
        + ((c3 & MASK_ALPHA) >> 24) * w3)
        << (24 - s))
        & MASK_ALPHA;
    let green = (((c1 & MASK_2) * w1 + (c2 & MASK_2) * w2 + (c3 & MASK_2) * w3) >> s) & MASK_2;
    let redblue =
        (((c1 & MASK_13) * w1 + (c2 & MASK_13) * w2 + (c3 & MASK_13) * w3) >> s) & MASK_13;
    alpha + green + redblue
}

/// `(c1 * 3 + c2) / 4`.
#[inline]
#[must_use]
pub(crate) fn interp1(c1: u32, c2: u32) -> u32 {
    interpolate_2(c1, 3, c2, 1, 2)
}

/// `(c1 * 2 + c2 + c3) / 4`.
#[inline]
#[must_use]
pub(crate) fn interp2(c1: u32, c2: u32, c3: u32) -> u32 {
    interpolate_3(c1, 2, c2, 1, c3, 1, 2)
}

/// `(c1 * 7 + c2) / 8`.
#[inline]
#[must_use]
pub(crate) fn interp3(c1: u32, c2: u32) -> u32 {
    interpolate_2(c1, 7, c2, 1, 3)
}

/// `(c1 * 2 + (c2 + c3) * 7) / 16`.
#[inline]
#[must_use]
pub(crate) fn interp4(c1: u32, c2: u32, c3: u32) -> u32 {
    interpolate_3(c1, 2, c2, 7, c3, 7, 4)
}

/// `(c1 + c2) / 2`.
#[inline]
#[must_use]
pub(crate) fn interp5(c1: u32, c2: u32) -> u32 {
    interpolate_2(c1, 1, c2, 1, 1)
}

/// `(c1 * 5 + c2 * 2 + c3) / 8`.
#[inline]
#[must_use]
pub(crate) fn interp6(c1: u32, c2: u32, c3: u32) -> u32 {
    interpolate_3(c1, 5, c2, 2, c3, 1, 3)
}

/// `(c1 * 6 + c2 + c3) / 8`.
#[inline]
#[must_use]
pub(crate) fn interp7(c1: u32, c2: u32, c3: u32) -> u32 {
    interpolate_3(c1, 6, c2, 1, c3, 1, 3)
}

/// `(c1 * 5 + c2 * 3) / 8`.
#[inline]
#[must_use]
pub(crate) fn interp8(c1: u32, c2: u32) -> u32 {
    interpolate_2(c1, 5, c2, 3, 3)
}

/// `(c1 * 2 + (c2 + c3) * 3) / 8`.
#[inline]
#[must_use]
pub(crate) fn interp9(c1: u32, c2: u32, c3: u32) -> u32 {
    interpolate_3(c1, 2, c2, 3, c3, 3, 3)
}

/// `(c1 * 14 + c2 + c3) / 16`.
#[inline]
#[must_use]
pub(crate) fn interp10(c1: u32, c2: u32, c3: u32) -> u32 {
    interpolate_3(c1, 14, c2, 1, c3, 1, 4)
}

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

    const A: u32 = 0xFF11_33DD;
    const B: u32 = 0x8844_AA22;
    const C: u32 = 0x40FE_01C0;

    #[test]
    fn named_blends_match_reference() {
        // Known-good values with mixed alpha, so the alpha-split path runs.
        assert_eq!(interp1(A, B), 0xE11D_50AE);
        assert_eq!(interp2(A, B, C), 0xB159_44A7);
        assert_eq!(interp3(A, B), 0xF017_41C5);
        assert_eq!(interp4(A, B, C), 0x778F_517E);
        assert_eq!(interp5(A, B), 0xC32A_6E7F);
        assert_eq!(interp6(A, B, C), 0xC93B_4AAA);
        assert_eq!(interp7(A, B, C), 0xD835_3BC2);
        assert_eq!(interp8(A, B), 0xD224_5F96);
        assert_eq!(interp9(A, B, C), 0x8A7D_4C8C);
        assert_eq!(interp10(A, B, C), 0xEB23_37CF);
    }

    #[test]
    fn two_color_blend_returns_input_when_equal() {
        // interpolate_2 short-circuits and preserves exact bits including alpha.
        assert_eq!(interp1(A, A), A);
        assert_eq!(interp5(A, A), A);
        assert_eq!(interpolate_2(A, 5, A, 3, 3), A);
    }

    #[test]
    fn three_color_blend_has_no_early_return() {
        // interpolate_3 runs the lane math even for equal inputs. The result
        // still equals the input, but through the arithmetic path.
        assert_eq!(interp2(A, A, A), A);
        assert_eq!(interpolate_3(A, 2, A, 1, A, 1, 2), A);
    }

    #[test]
    fn blend_weights_follow_documented_ratios() {
        // interp5 is a plain average. Opaque black and white average to mid
        // gray with full alpha.
        let black = 0xFF00_0000;
        let white = 0xFFFF_FFFF;
        assert_eq!(interp5(black, white), 0xFF7F_7F7F);
        // interp1 weights c1 three to one.
        assert_eq!(interp1(0xFF00_0000, 0xFF40_4040), 0xFF10_1010);
    }

    #[test]
    fn alpha_blends_separately_from_color() {
        // Alpha 0x00 and 0xFF averaged gives 0x7F, while the color channels
        // blend independently.
        let lo = 0x0011_2233;
        let hi = 0xFF11_2233;
        // Colors are equal, so interpolate_2 early-returns the first input. Use
        // a three-color blend to force the alpha lane.
        let r = interpolate_3(lo, 1, hi, 1, hi, 2, 2);
        assert_eq!(r >> 24, 0xBF, "alpha lane");
        assert_eq!(r & 0x00FF_FFFF, 0x0011_2233, "color lanes unchanged");
    }

    #[test]
    fn disjoint_lanes_recombine_without_carry() {
        // The three lane masks are disjoint and cover the whole word, so the
        // plain add that recombines them never carries between fields. Compare
        // the field-wise blend against an explicit per-field reconstruction.
        let cases = [(A, B), (B, C), (C, A), (0x0102_0304, 0xF0E0_D0C0)];
        for (c1, c2) in cases {
            let blended = interpolate_2(c1, 3, c2, 1, 2);
            let alpha = blended & 0xFF00_0000;
            let green = blended & 0x0000_FF00;
            let redblue = blended & 0x00FF_00FF;
            assert_eq!(alpha | green | redblue, blended);
            assert_eq!(alpha + green + redblue, blended);
        }
    }
}