hqx 0.1.1

high-quality pixel-art image magnification (2x/3x/4x upscaling)
Documentation
//! Border replication and row-stride behavior.
//!
//! Covers degenerate strips, the four image edges, padded destination rows,
//! and padded source rows. Strip and edge results come from the committed
//! goldens. Padding tests compare a padded run to the tight run.

mod common;

use common::{load_golden, load_input};

#[test]
fn single_pixel_scales_to_solid_block() {
    let src = [0x00AB_CDEFu32];
    for (scale, run) in scalers() {
        let mut dst = vec![0u32; scale * scale];
        run(&src, &mut dst, 1, 1);
        assert!(dst.iter().all(|&p| p == src[0]), "scale {scale}");
    }
}

#[test]
fn vertical_strip_matches_golden() {
    check_case("strip_1x6");
}

#[test]
fn horizontal_strip_matches_golden() {
    check_case("strip_6x1");
}

#[test]
fn zero_dimensions_write_nothing() {
    let src = [0x00FF_0000u32; 4];
    let mut dst = [0xDEAD_BEEFu32; 64];
    hqx::hq2x(&src, &mut dst, 0, 2);
    assert!(dst.iter().all(|&p| p == 0xDEAD_BEEF), "width 0");
    hqx::hq3x(&src, &mut dst, 2, 0);
    assert!(dst.iter().all(|&p| p == 0xDEAD_BEEF), "height 0");
    hqx::hq4x(&src, &mut dst, 0, 0);
    assert!(dst.iter().all(|&p| p == 0xDEAD_BEEF), "both 0");
}

#[test]
#[should_panic(expected = "dest too small")]
fn short_dest_panics_before_writing() {
    // A destination one pixel short of the required size panics up front, so the
    // buffer is never partly written. The message names the needed pixel count.
    let src = [0x00FF_0000u32, 0x0000_FF00, 0x0000_00FF, 0x00FF_FFFF];
    let mut dst = vec![0u32; 4 * 2 * 2 - 1];
    hqx::hq2x(&src, &mut dst, 2, 2);
}

#[test]
#[should_panic(expected = "src too small")]
fn short_src_panics_before_writing() {
    // A 2x3 image needs six source pixels. Five passes the destination check but
    // would index out of bounds part way through the loop. The source check
    // fails first, before any write, and names the needed pixel count.
    let src = [
        0x00FF_0000u32,
        0x0000_FF00,
        0x0000_00FF,
        0x0011_2233,
        0x0044_5566,
    ];
    let mut dst = vec![0u32; 4 * 2 * 3];
    hqx::hq2x(&src, &mut dst, 2, 3);
}

#[test]
fn exact_dest_length_is_accepted() {
    // The minimal correct buffer must not trip the length check.
    let src = [0x00FF_0000u32, 0x0000_FF00, 0x0000_00FF, 0x00FF_FFFF];
    for (scale, run) in scalers() {
        let mut dst = vec![0u32; scale * scale * 2 * 2];
        run(&src, &mut dst, 2, 2);
    }
}

#[test]
fn padded_dest_rows_leave_pad_untouched() {
    // Run each scale into a destination with extra columns per row. The active
    // region must equal the tight golden and the pad must keep its sentinel.
    let name = "two_color_vert";
    let img = load_input(name);
    let pad = 3usize;
    let cases: &[(usize, &str)] = &[(2, "hq2x"), (3, "hq3x"), (4, "hq4x")];
    for &(scale, ext) in cases {
        let dest_w = img.w * scale;
        let stride = dest_w + pad;
        let dest_h = img.h * scale;
        let mut dst = vec![0xCAFE_F00Du32; stride * dest_h];
        let dpl = stride;
        let spl = img.w;
        match scale {
            2 => hqx::hq2x_rb(&img.px, spl, &mut dst, dpl, img.w as u32, img.h as u32),
            3 => hqx::hq3x_rb(&img.px, spl, &mut dst, dpl, img.w as u32, img.h as u32),
            _ => hqx::hq4x_rb(&img.px, spl, &mut dst, dpl, img.w as u32, img.h as u32),
        }
        let gold = load_golden(name, ext);
        for y in 0..dest_h {
            for x in 0..dest_w {
                assert_eq!(
                    dst[y * stride + x],
                    gold[y * dest_w + x],
                    "active {x},{y} s{scale}"
                );
            }
            for x in dest_w..stride {
                assert_eq!(dst[y * stride + x], 0xCAFE_F00D, "pad {x},{y} s{scale}");
            }
        }
    }
}

#[test]
fn padded_source_rows_match_tight_source() {
    // Build a source with trailing pad pixels per row. The row stride skips the
    // pad, so the output must equal the unpadded run.
    let name = "diagonal";
    let img = load_input(name);
    let pad = 2usize;
    let stride = img.w + pad;
    // Fill the pad with a loud sentinel. A faithful run skips it via the row
    // stride reset. If the column loop ever read a pad pixel, the output would
    // diverge from the tight golden, so this value guards against that bug.
    let mut padded = vec![0xFEED_BEEFu32; stride * img.h];
    for y in 0..img.h {
        for x in 0..img.w {
            padded[y * stride + x] = img.px[y * img.w + x];
        }
    }
    let cases: &[(usize, &str)] = &[(2, "hq2x"), (3, "hq3x"), (4, "hq4x")];
    for &(scale, ext) in cases {
        let mut dst = vec![0u32; scale * scale * img.w * img.h];
        let spl = stride;
        let dpl = img.w * scale;
        match scale {
            2 => hqx::hq2x_rb(&padded, spl, &mut dst, dpl, img.w as u32, img.h as u32),
            3 => hqx::hq3x_rb(&padded, spl, &mut dst, dpl, img.w as u32, img.h as u32),
            _ => hqx::hq4x_rb(&padded, spl, &mut dst, dpl, img.w as u32, img.h as u32),
        }
        assert_eq!(dst, load_golden(name, ext), "padded source s{scale}");
    }
}

#[test]
fn corner_pixels_replicate_source_corners() {
    // Edge replication makes each extreme output corner a copy of the matching
    // source corner.
    let name = "four_corners";
    let img = load_input(name);
    for (scale, run) in scalers() {
        let dw = img.w * scale;
        let dh = img.h * scale;
        let mut dst = vec![0u32; dw * dh];
        run(&img.px, &mut dst, img.w as u32, img.h as u32);
        let tl = img.px[0];
        let tr = img.px[img.w - 1];
        let bl = img.px[(img.h - 1) * img.w];
        let br = img.px[img.h * img.w - 1];
        assert_eq!(dst[0], tl, "top-left s{scale}");
        assert_eq!(dst[dw - 1], tr, "top-right s{scale}");
        assert_eq!(dst[(dh - 1) * dw], bl, "bottom-left s{scale}");
        assert_eq!(dst[dh * dw - 1], br, "bottom-right s{scale}");
    }
}

#[test]
fn alpha_only_neighbors_set_no_pattern_bits_but_carry_alpha() {
    // A neighbor that shares the center's RGB but differs in alpha satisfies
    // the raw inequality w[k] != w[5] yet fails the YUV difference test, so it
    // sets no pattern bit. The center resolves to pattern 0. Interpolation
    // still blends the alpha byte, so the output alpha differs from the source
    // while the RGB stays fixed. This pins the split between the alpha-sensitive
    // equality check and the alpha-blind color test.
    let img = load_input("alpha_pattern");
    let center_rgb = img.px[4] & 0x00FF_FFFF;
    for (scale, ext) in [(2usize, "hq2x"), (3, "hq3x"), (4, "hq4x")] {
        let mut dst = vec![0u32; scale * scale * img.w * img.h];
        match scale {
            2 => hqx::hq2x(&img.px, &mut dst, img.w as u32, img.h as u32),
            3 => hqx::hq3x(&img.px, &mut dst, img.w as u32, img.h as u32),
            _ => hqx::hq4x(&img.px, &mut dst, img.w as u32, img.h as u32),
        }
        assert_eq!(
            dst,
            load_golden("alpha_pattern", ext),
            "alpha_pattern s{scale}"
        );
        let dw = img.w * scale;
        // The cell at the center block's top-left blends alpha away from the
        // source center alpha of 0x00.
        let cell = dst[scale * dw + scale];
        assert_eq!(cell & 0x00FF_FFFF, center_rgb, "rgb fixed s{scale}");
        assert_ne!(cell >> 24, 0x00, "alpha blended s{scale}");
    }
}

type Scaler = fn(&[u32], &mut [u32], u32, u32);

fn scalers() -> Vec<(usize, Scaler)> {
    vec![
        (2, hqx::hq2x as Scaler),
        (3, hqx::hq3x as Scaler),
        (4, hqx::hq4x as Scaler),
    ]
}

fn check_case(name: &str) {
    let img = load_input(name);
    let cases: &[(usize, &str)] = &[(2, "hq2x"), (3, "hq3x"), (4, "hq4x")];
    for &(scale, ext) in cases {
        let mut dst = vec![0u32; scale * scale * img.w * img.h];
        match scale {
            2 => hqx::hq2x(&img.px, &mut dst, img.w as u32, img.h as u32),
            3 => hqx::hq3x(&img.px, &mut dst, img.w as u32, img.h as u32),
            _ => hqx::hq4x(&img.px, &mut dst, img.w as u32, img.h as u32),
        }
        assert_eq!(dst, load_golden(name, ext), "{name} s{scale}");
    }
}