5.6 KiB · text History 38cca4c
//! Checks the reconstructed renderer against what the original studio shipped.
//!
//! The studio's source was lost; its output was not. `cbzauth_background.png`
//! is the real 432px background from the installed cbzauth APK, and is the
//! only ground truth available for how the tool actually drew a diagonal
//! split. If this test fails, the reconstruction has drifted from the thing
//! it is meant to reproduce.

use icon_studio::palette::NamedColor;
use icon_studio::render;
use icon_studio::spec::{Background, IconSpec};

const TEAL: [u8; 3] = [0x00, 0xC9, 0xD4]; // Casabeza ACCENT
const GREEN: [u8; 3] = [0xAA, 0xD4, 0x00]; // Casabeza LOGO_GREEN

fn casabeza_split() -> IconSpec {
    IconSpec {
        bg_a: NamedColor { role: "primary", rgb: TEAL },
        bg_b: NamedColor { role: "accent", rgb: GREEN },
        background: Background::DiagonalSplit,
        angle: 45.0,
        ..Default::default()
    }
}

/// Counts of each fully-opaque colour in the image.
fn histogram(px: &[u8]) -> std::collections::HashMap<[u8; 3], usize> {
    let mut h = std::collections::HashMap::new();
    for c in px.chunks(4) {
        *h.entry([c[0], c[1], c[2]]).or_insert(0) += 1;
    }
    h
}

#[test]
fn diagonal_split_matches_the_shipped_background() {
    let shipped = image::open("tests/fixtures/cbzauth_background.png")
        .expect("fixture present")
        .to_rgba8();
    assert_eq!(shipped.dimensions(), (432, 432), "fixture is the xxxhdpi asset");

    let ours = render::render_background(&casabeza_split(), 432);

    let shipped_hist = histogram(shipped.as_raw());
    let ours_hist = histogram(
        &ours
            .pixels()
            .iter()
            .flat_map(|p| [p.red(), p.green(), p.blue(), p.alpha()])
            .collect::<Vec<u8>>(),
    );

    // The shipped icon is an even two-tone split: both brand colours cover
    // the same area, with only the boundary blended.
    let s_teal = shipped_hist.get(&TEAL).copied().unwrap_or(0);
    let s_green = shipped_hist.get(&GREEN).copied().unwrap_or(0);
    let o_teal = ours_hist.get(&TEAL).copied().unwrap_or(0);
    let o_green = ours_hist.get(&GREEN).copied().unwrap_or(0);

    assert!(s_teal > 0 && s_green > 0, "fixture should be two-tone");

    let total = 432.0 * 432.0;
    for (name, teal, green) in [
        ("shipped", s_teal, s_green),
        ("ours", o_teal, o_green),
    ] {
        let share = (teal + green) as f64 / total;
        assert!(share > 0.99, "{name}: brand colours should cover the canvas, got {share:.4}");
        let balance = (teal as f64 - green as f64).abs() / total;
        assert!(balance < 0.01, "{name}: halves should be even, off by {balance:.4}");
    }
}

#[test]
fn split_boundary_is_hard_not_a_gradient() {
    // The shipped background has ~one blended pixel per row: a crisp edge, not
    // a two-stop gradient. This is the detail that distinguishes the attested
    // "diagonal split" from the reconstructed "linear gradient".
    let ours = render::render_background(&casabeza_split(), 432);
    let px: Vec<u8> = ours
        .pixels()
        .iter()
        .flat_map(|p| [p.red(), p.green(), p.blue(), p.alpha()])
        .collect();
    let hist = histogram(&px);
    let pure = hist.get(&TEAL).copied().unwrap_or(0) + hist.get(&GREEN).copied().unwrap_or(0);
    let blended = (432 * 432) as usize - pure;
    assert!(
        blended < 432 * 4,
        "edge should be hairline; {blended} blended pixels looks like a gradient"
    );
}

#[test]
fn solid_background_ignores_the_second_colour() {
    let spec = IconSpec { background: Background::Solid, ..casabeza_split() };
    let pm = render::render_background(&spec, 64);
    assert!(pm.pixels().iter().all(|p| [p.red(), p.green(), p.blue()] == TEAL));
}

#[test]
fn every_background_fills_the_canvas() {
    // A generator that leaves transparent gaps would show the launcher's
    // backdrop through the icon.
    for bg in Background::ALL {
        let spec = IconSpec { background: *bg, ..casabeza_split() };
        let pm = render::render_background(&spec, 128);
        let clear = pm.pixels().iter().filter(|p| p.alpha() < 255).count();
        assert_eq!(clear, 0, "{:?} left {clear} transparent pixels", bg);
    }
}

#[test]
fn foreground_is_transparent_outside_the_mark() {
    // The foreground layer must stay transparent so Android can composite and
    // parallax it over the background.
    let pm = render::render_foreground(&casabeza_split(), 432);
    let opaque = pm.pixels().iter().filter(|p| p.alpha() > 0).count();
    assert!(opaque > 0, "glyph should render");
    assert!(
        (opaque as f64) < 432.0 * 432.0 * 0.5,
        "foreground should be mostly transparent"
    );
}

#[test]
fn export_writes_every_density_and_layer() {
    let dir = std::env::temp_dir().join(format!("icon-studio-test-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);

    let files = icon_studio::export::export_mipmaps(&casabeza_split(), &dir, "ic_icon")
        .expect("export succeeds");

    assert_eq!(files.len(), icon_studio::export::DENSITIES.len() * 3);
    for (bucket, size) in icon_studio::export::DENSITIES {
        for suffix in ["", "_background", "_foreground"] {
            let p = dir.join(format!("mipmap-{bucket}")).join(format!("ic_icon{suffix}.png"));
            assert!(p.exists(), "{} missing", p.display());
            let img = image::open(&p).expect("valid png");
            assert_eq!(img.width(), *size, "{} wrong size", p.display());
        }
    }
    // xxxhdpi must land on 432px, the size the shipped cbz icons use.
    assert!(dir.join("mipmap-xxxhdpi/ic_icon.png").exists());
    std::fs::remove_dir_all(&dir).ok();
}