//! 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();
}
neiam
/icon-studio
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
5.6 KiB · text
History
38cca4c