mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 23:26:39 +03:00
The engine no longer lives in a separate repo. `crates/` is a snapshot of the
open crates (foundation, core, common, context, live, hook, skills, comp,
comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git
archive of the engine repo at the commit that finished the boundary split.
None of that repo's history comes with it, and none of it should: the closed
half stays private.
The closed half is the rule engine. It ships as a prebuilt native archive per
target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub
Release on this repo. `crates/core/build.rs` resolves and links it three ways:
`IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the
`~/.impeccable/detector/<version>/<target>/` cache, else a download verified
against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol
C ABI; nothing above it knows the boundary exists.
What changed versus the engine repo copy:
- Every crate manifest moves from `license-file.workspace` to
`license.workspace` (this workspace declares Apache-2.0), and the workspace
gains the `postcard` dependency the boundary encoding needs.
- The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of
a sibling `launcher/` dir, and `engine_binary` downloads from
`github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead
of the retired dist repo. No oracle golden carried the old URL, so no
re-recording was owed.
- The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`,
`../impeccable-second` or a hardcoded home directory now resolve the root as
`CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an
override for an out-of-tree checkout.
- The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm
glue) is no longer tracked. `crates/core/build.rs` resolves it beside the
archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and
live mode serves that. `scripts/check-detector-release.mjs` now requires it
and its `.sha256` in a detector release.
- The live crate embeds `skill/scripts/live-browser*.js` and
`modern-screenshot.umd.js` directly rather than through vendored copies, so
the binary and the installed skill cannot drift.
- `crates/browser/assets/` (an unused second copy of the bundle) is gone.
- `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a
plain `cargo build --release -p impeccable` is enough to run `bun run test`.
Verified with the archive from a local detector build: `cargo test --workspace`
267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the
default suite green, and the launcher's `engine-probe` handshake answering
through `skill/scripts/impeccable`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
417 lines
18 KiB
Rust
417 lines
18 KiB
Rust
//! Differential-parity harness: runs the Rust port over the same fixtures the
|
|
//! JS libs were recorded on (crates/comp/tests/record.mjs) and asserts the
|
|
//! outputs match the golden (crates/comp/tests/golden/parity.json).
|
|
//!
|
|
//! Pixel-level parity is proven by CRC32 of decoded/produced RGBA (and the f32
|
|
//! diff-map) buffers; scores and fingerprints are compared value-for-value.
|
|
|
|
use impeccable_comp::font_fingerprint as ff;
|
|
use impeccable_comp::font_index as fi;
|
|
use impeccable_comp::hero;
|
|
use impeccable_comp::metrics as m;
|
|
use impeccable_comp::raster::{self as r, rgb, Image};
|
|
use impeccable_comp::{crc32, crc32_f32, png_io};
|
|
use serde_json::Value;
|
|
use std::path::PathBuf;
|
|
|
|
fn fixtures() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
|
|
}
|
|
fn golden() -> Value {
|
|
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/golden/parity.json");
|
|
serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()
|
|
}
|
|
fn load(name: &str) -> Image {
|
|
let buf = std::fs::read(fixtures().join(name)).unwrap();
|
|
png_io::decode_png(&buf).unwrap().image
|
|
}
|
|
fn u(v: &Value) -> u32 {
|
|
v.as_u64().unwrap() as u32
|
|
}
|
|
fn f(v: &Value) -> f64 {
|
|
v.as_f64().unwrap()
|
|
}
|
|
fn approx(a: f64, b: &Value, tol: f64, what: &str) {
|
|
let bf = f(b);
|
|
assert!((a - bf).abs() <= tol, "{what}: rust {a} vs golden {bf} (|d|={})", (a - bf).abs());
|
|
}
|
|
|
|
#[test]
|
|
fn png_decode_pixel_parity() {
|
|
let g = golden();
|
|
let sample = load("sample.png");
|
|
assert_eq!(sample.width, g["sample"]["width"].as_u64().unwrap() as usize);
|
|
assert_eq!(sample.height, g["sample"]["height"].as_u64().unwrap() as usize);
|
|
assert_eq!(crc32(&sample.data), u(&g["sample"]["crc"]), "sample.png RGBA crc");
|
|
for (name, key) in [
|
|
("comp.png", "comp"),
|
|
("build_flat.png", "flat"),
|
|
("build_recolor.png", "recolor"),
|
|
("build_shift.png", "shift"),
|
|
] {
|
|
assert_eq!(crc32(&load(name).data), u(&g["images"][key]), "{name} crc");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn png_encode_roundtrip() {
|
|
// The encoder need not match the JS byte stream, but decode(encode(x)) must
|
|
// round-trip the pixels exactly.
|
|
let comp = load("comp.png");
|
|
let bytes = png_io::encode_png(&comp, &[]).unwrap();
|
|
let back = png_io::decode_png(&bytes).unwrap().image;
|
|
assert_eq!(comp.data, back.data, "encode->decode pixel roundtrip");
|
|
}
|
|
|
|
#[test]
|
|
fn raster_op_parity() {
|
|
let g = golden();
|
|
let comp = load("comp.png");
|
|
assert_eq!(crc32(&r::resize(&comp, 256.0, 170.0).data), u(&g["raster"]["resize_down"]));
|
|
let up = r::resize(&r::crop(&comp, 24.0, 60.0, 300.0, 90.0), 600.0, 180.0);
|
|
assert_eq!(crc32(&up.data), u(&g["raster"]["resize_up"]));
|
|
assert_eq!(crc32(&r::crop(&comp, 340.0, 48.0, 400.0, 192.0).data), u(&g["raster"]["crop"]));
|
|
assert_eq!(crc32(&r::fit(&comp, 300.0, 300.0, false).data), u(&g["raster"]["fit"]));
|
|
|
|
let mut c = r::create_image(200, 60, [10, 10, 10, 255]);
|
|
r::stroke_rect(&mut c, 10.0, 10.0, 180.0, 40.0, rgb([255, 0, 0]), 3.0);
|
|
r::draw_label(&mut c, "HELLO 42%", 20.0, 20.0, rgb([255, 255, 255]), [0.0, 0.0, 0.0, 220.0], 2.0, 4.0);
|
|
assert_eq!(crc32(&c.data), u(&g["raster"]["label"]), "drawLabel/strokeRect");
|
|
|
|
let mut b = r::create_image(120, 80, [255, 255, 255, 255]);
|
|
r::blit(&mut b, &r::crop(&comp, 0.0, 0.0, 60.0, 40.0), 30.0, 20.0);
|
|
assert_eq!(crc32(&b.data), u(&g["raster"]["blit"]), "blit");
|
|
}
|
|
|
|
fn check_scores(a: &Image, b: &Image, gs: &Value) {
|
|
approx(m::structure_score(a, b, 256), &gs["structure"], 1e-6, "structure");
|
|
let cs = m::color_score(a, b);
|
|
approx(cs.score, &gs["color"]["score"], 1e-6, "color.score");
|
|
approx(cs.intersection, &gs["color"]["intersection"], 1e-6, "color.intersection");
|
|
approx(cs.palette_match, &gs["color"]["paletteMatch"], 1e-6, "color.paletteMatch");
|
|
let ds = m::detail_score(a, b, 12, 8);
|
|
approx(ds.score, &gs["detail"]["score"], 1e-6, "detail.score");
|
|
approx(ds.raw_score, &gs["detail"]["rawScore"], 1e-6, "detail.rawScore");
|
|
approx(ds.added_fraction, &gs["detail"]["addedFraction"], 1e-6, "detail.addedFraction");
|
|
let ba = m::horizontal_bands(a, 128, 0.02);
|
|
let bb = m::horizontal_bands(b, 128, 0.02);
|
|
approx(m::band_score(&ba, &bb, 0.04), &gs["bands"], 1e-7, "bands");
|
|
let dm = m::diff_map(a, b, 384);
|
|
assert_eq!(crc32_f32(&dm.data), u(&gs["diffMapCrc"]), "diffMap f32 crc (bit-exact)");
|
|
}
|
|
|
|
#[test]
|
|
fn metrics_score_parity() {
|
|
let g = golden();
|
|
let comp = load("comp.png");
|
|
let flat = load("build_flat.png");
|
|
let recolor = load("build_recolor.png");
|
|
let shift = load("build_shift.png");
|
|
let sample = load("sample.png");
|
|
check_scores(&comp, &flat, &g["scores"]["comp_flat"]);
|
|
check_scores(&comp, &recolor, &g["scores"]["comp_recolor"]);
|
|
check_scores(&comp, &shift, &g["scores"]["comp_shift"]);
|
|
check_scores(&comp, &comp, &g["scores"]["comp_self"]);
|
|
let sr = r::resize(&sample, 300.0, 200.0);
|
|
check_scores(&sample, &sr, &g["scores"]["sample_resized"]);
|
|
|
|
assert_eq!(crc32_f32(&m::to_gray(&comp).data), u(&g["gray"]["comp_toGray"]), "toGray f32 crc");
|
|
assert_eq!(
|
|
crc32_f32(&m::blur_gray(&m::to_gray(&comp), 2).data),
|
|
u(&g["gray"]["comp_blur"]),
|
|
"blurGray f32 crc"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dominant_colors_and_bands_parity() {
|
|
let g = golden();
|
|
for (name, key) in [("comp.png", "comp"), ("sample.png", "sample")] {
|
|
let img = load(name);
|
|
let dc = m::dominant_colors(&img, 6, 3);
|
|
let gold = g["dominant"][key].as_array().unwrap();
|
|
assert_eq!(dc.len(), gold.len(), "{key} dominant count");
|
|
for (c, gc) in dc.iter().zip(gold) {
|
|
assert_eq!(c.hex, gc["hex"].as_str().unwrap(), "{key} hex");
|
|
approx(c.coverage, &gc["coverage"], 1e-12, "coverage");
|
|
}
|
|
}
|
|
let comp = load("comp.png");
|
|
let bands = m::horizontal_bands(&comp, 128, 0.02);
|
|
let gb = g["bands"]["comp"].as_array().unwrap();
|
|
assert_eq!(bands.len(), gb.len(), "band count");
|
|
for (b, gbi) in bands.iter().zip(gb) {
|
|
approx(b.y, &gbi["y"], 1e-7, "band.y");
|
|
approx(b.strength, &gbi["strength"], 1e-7, "band.strength");
|
|
}
|
|
let dg = m::detail_grid(&comp, 12, 8, 512);
|
|
let gg = g["detailGrid"]["comp"].as_array().unwrap();
|
|
for (i, cell) in dg.cells.iter().enumerate() {
|
|
approx(*cell as f64, &gg[i], 1e-4, "detailGrid cell");
|
|
}
|
|
}
|
|
|
|
fn check_ink_box(img: &Image, gv: &Value, what: &str) {
|
|
let b = hero::ink_box(img).unwrap();
|
|
assert_eq!(b.x, gv["x"].as_i64().unwrap(), "{what}.x");
|
|
assert_eq!(b.y, gv["y"].as_i64().unwrap(), "{what}.y");
|
|
assert_eq!(b.w, gv["w"].as_i64().unwrap(), "{what}.w");
|
|
assert_eq!(b.h, gv["h"].as_i64().unwrap(), "{what}.h");
|
|
}
|
|
|
|
#[test]
|
|
fn ink_box_parity() {
|
|
let g = golden();
|
|
check_ink_box(&load("comp.png"), &g["inkBox"]["comp"], "comp");
|
|
check_ink_box(&load("sample.png"), &g["inkBox"]["sample"], "sample");
|
|
check_ink_box(&load("build_flat.png"), &g["inkBox"]["flat"], "flat");
|
|
}
|
|
|
|
fn cmp_fp(fp: Option<&ff::Fingerprint>, gold: &Value, what: &str) {
|
|
if gold.is_null() {
|
|
assert!(fp.is_none(), "{what}: expected null fingerprint");
|
|
return;
|
|
}
|
|
let fp = fp.expect(&format!("{what}: expected some fingerprint"));
|
|
assert_eq!(fp.lines as u64, gold["lines"].as_u64().unwrap(), "{what}.lines");
|
|
assert_eq!(fp.glyphs, gold["glyphs"].as_i64().unwrap(), "{what}.glyphs");
|
|
approx(fp.cap_height_px, &gold["capHeightPx"], 1e-9, &format!("{what}.capHeightPx"));
|
|
assert_eq!(fp.ink_is_dark, gold["inkIsDark"].as_bool().unwrap(), "{what}.inkIsDark");
|
|
assert_eq!(fp.upsampled, gold["upsampled"].as_bool().unwrap(), "{what}.upsampled");
|
|
assert_eq!(fp.all_caps, gold["allCaps"].as_bool().unwrap(), "{what}.allCaps");
|
|
assert_eq!(fp.isolated_from, gold["isolatedFrom"].as_i64().unwrap(), "{what}.isolatedFrom");
|
|
match (fp.weight, gold["weight"].as_f64()) {
|
|
(Some(w), Some(_)) => approx(w, &gold["weight"], 1e-12, &format!("{what}.weight")),
|
|
(None, None) => {}
|
|
(a, b) => panic!("{what}.weight mismatch: rust {a:?} golden {b:?}"),
|
|
}
|
|
for k in ff::FEATURES.iter() {
|
|
let rv = fp.get(k);
|
|
let gv = &gold[k.as_str()];
|
|
match (rv, gv.is_null()) {
|
|
(Some(v), false) => approx(v, gv, 1e-12, &format!("{what}.{k}")),
|
|
(None, true) => {}
|
|
_ => panic!("{what}.{k} mismatch: rust {rv:?} golden {gv}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn fingerprint_parity() {
|
|
let g = golden();
|
|
let o = ff::FpOpts::default();
|
|
// (fixture file, golden key)
|
|
for (file, key) in [
|
|
("text_s6", "s6"),
|
|
("text_s4", "s4"),
|
|
("text_heavy", "heavy"),
|
|
("text_multi", "multi"),
|
|
("sample", "sample"),
|
|
] {
|
|
let img = load(&format!("{file}.png"));
|
|
let fp = ff::fingerprint(&img, &o);
|
|
cmp_fp(fp.as_ref(), &g["fingerprint"][key], key);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn distance_parity() {
|
|
let g = golden();
|
|
let o = ff::FpOpts::default();
|
|
let s6 = ff::fingerprint(&load("text_s6.png"), &o).unwrap();
|
|
let s4 = ff::fingerprint(&load("text_s4.png"), &o).unwrap();
|
|
let heavy = ff::fingerprint(&load("text_heavy.png"), &o).unwrap();
|
|
let d = |a: &ff::Fingerprint, b: &ff::Fingerprint| ff::distance(&|k| a.get(k), &|k| b.get(k));
|
|
approx(d(&s6, &s6), &g["distance"]["self"], 1e-12, "distance self");
|
|
approx(d(&s6, &s4), &g["distance"]["s6_s4"], 1e-10, "distance s6_s4");
|
|
approx(d(&s6, &heavy), &g["distance"]["s6_heavy"], 1e-10, "distance s6_heavy");
|
|
let gg = ff::gross_gap(&|k| s6.get(k), &|k| heavy.get(k));
|
|
approx(gg.width.unwrap(), &g["distance"]["grossGap_s6_heavy"]["width"], 1e-10, "grossGap.width");
|
|
approx(gg.weight.unwrap(), &g["distance"]["grossGap_s6_heavy"]["weight"], 1e-10, "grossGap.weight");
|
|
|
|
// synthetic: {advX,densTall,contrast} vs contrast:null
|
|
let a = |k: &str| match k {
|
|
"advX" => Some(0.5),
|
|
"densTall" => Some(0.5),
|
|
"contrast" => Some(1.0),
|
|
_ => None,
|
|
};
|
|
let b = |k: &str| match k {
|
|
"advX" => Some(0.5),
|
|
"densTall" => Some(0.5),
|
|
_ => None,
|
|
};
|
|
approx(ff::distance(&a, &b), &g["distance"]["synthetic"], 1e-12, "distance synthetic");
|
|
|
|
let gf: Vec<String> = g["features"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect();
|
|
assert_eq!(*ff::FEATURES, gf, "FEATURES list");
|
|
}
|
|
|
|
#[test]
|
|
fn font_index_parity() {
|
|
let g = golden();
|
|
let gi = &g["index"];
|
|
// INDEX_FEATURES order
|
|
let gif: Vec<String> = gi["features"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect();
|
|
assert_eq!(*fi::INDEX_FEATURES, gif, "INDEX_FEATURES");
|
|
|
|
let o = ff::FpOpts::default();
|
|
let s6 = ff::fingerprint(&load("text_s6.png"), &o).unwrap();
|
|
assert_eq!(fi::pack_vector(&|k| s6.get(k), &fi::INDEX_FEATURES), gi["pack"].as_str().unwrap(), "packVector");
|
|
let un = fi::unpack_vector(gi["pack"].as_str().unwrap(), &fi::INDEX_FEATURES);
|
|
for k in fi::INDEX_FEATURES.iter() {
|
|
let gv = &gi["unpack"][k.as_str()];
|
|
match (un.get(k), gv.is_null()) {
|
|
(Some(v), false) => approx(v, gv, 1e-9, &format!("unpack.{k}")),
|
|
(None, true) => {}
|
|
_ => panic!("unpack.{k} mismatch"),
|
|
}
|
|
}
|
|
// routeSize
|
|
let sizes = fi::index_sizes();
|
|
assert_eq!(fi::route_size(8.0, &sizes, false), fi::SizeKey::Num(f(&gi["route"]["c8"])), "route c8");
|
|
assert_eq!(fi::route_size(20.0, &sizes, false), fi::SizeKey::Num(f(&gi["route"]["c20"])), "route c20");
|
|
assert_eq!(fi::route_size(30.0, &sizes, false), fi::SizeKey::Num(f(&gi["route"]["c30"])), "route c30");
|
|
assert_eq!(fi::route_size(30.0, &sizes, true).key(), gi["route"]["c30caps"].as_str().unwrap(), "route c30caps");
|
|
// NON_TEXT_FAMILY
|
|
for e in gi["nonText"].as_array().unwrap() {
|
|
let fam = e["f"].as_str().unwrap();
|
|
assert_eq!(fi::NON_TEXT_FAMILY.is_match(fam), e["hit"].as_bool().unwrap(), "nonText {fam}");
|
|
}
|
|
|
|
// Full index (candidate ranking) — needs the catalog JSON from the public repo.
|
|
let pubrepo = std::env::var("IMPECCABLE_PUBLIC_REPO")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."));
|
|
let idx_path = pubrepo.join("skill/scripts/data/font-index.json");
|
|
if !idx_path.exists() {
|
|
eprintln!("skipping candidate-ranking parity: {idx_path:?} not present");
|
|
return;
|
|
}
|
|
let index = fi::load_font_index(&idx_path).expect("load font index");
|
|
if let Some(loaded) = gi.get("loaded") {
|
|
assert_eq!(index.schema, loaded["schema"].as_i64().unwrap(), "index schema");
|
|
assert_eq!(index.entries.len(), loaded["entries"].as_u64().unwrap() as usize, "index entries");
|
|
}
|
|
if let Some(cand) = gi.get("candidates_s6").and_then(|c| c.as_array()) {
|
|
let out = fi::candidates_from_index(&s6, &index, &fi::CandOpts { n: 10, ..Default::default() });
|
|
assert_eq!(out.len(), cand.len(), "candidate count s6");
|
|
for (c, gc) in out.iter().zip(cand) {
|
|
assert_eq!(c.family, gc["family"].as_str().unwrap(), "candidate family");
|
|
approx(c.weight, &gc["weight"], 1e-9, "candidate weight");
|
|
assert_eq!(c.category, gc["category"].as_str().unwrap(), "candidate category");
|
|
approx(c.d, &gc["d"], 1e-6, "candidate distance");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn assert_json_eq(a: &Value, b: &Value, what: &str) {
|
|
// numeric-aware equality: treat 42 and 42.0 as equal, floats within 1e-9.
|
|
match (a, b) {
|
|
(Value::Number(_), Value::Number(_)) => {
|
|
let (x, y) = (a.as_f64().unwrap(), b.as_f64().unwrap());
|
|
assert!((x - y).abs() <= 1e-9 + 1e-9 * y.abs(), "{what}: {x} vs {y}");
|
|
}
|
|
(Value::Array(xa), Value::Array(ya)) => {
|
|
assert_eq!(xa.len(), ya.len(), "{what}: array len");
|
|
for (i, (x, y)) in xa.iter().zip(ya).enumerate() {
|
|
assert_json_eq(x, y, &format!("{what}[{i}]"));
|
|
}
|
|
}
|
|
(Value::Object(xo), Value::Object(yo)) => {
|
|
for (k, yv) in yo {
|
|
let xv = xo.get(k).unwrap_or_else(|| panic!("{what}: missing key {k}"));
|
|
assert_json_eq(xv, yv, &format!("{what}.{k}"));
|
|
}
|
|
}
|
|
_ => assert_eq!(a, b, "{what}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hero_checks_parity() {
|
|
let g = golden();
|
|
let gh = &g["hero"];
|
|
|
|
// textRegion
|
|
let comp_crop = load("hero_comp_crop.png");
|
|
let build_crop = load("hero_build_crop.png");
|
|
let region = hero::Region { id: "hero".into(), kind: "text".into(), chosen: None };
|
|
let trc = hero::text_region_check(®ion, &comp_crop, &build_crop);
|
|
assert_json_eq(&trc["findings"], &gh["textRegion"]["findings"], "textRegion.findings");
|
|
assert_json_eq(&trc["metrics"], &gh["textRegion"]["metrics"], "textRegion.metrics");
|
|
|
|
// inventedInk: rebuild the exact inputs the recorder used.
|
|
let hero_comp = r::create_image(512, 400, [245, 245, 240, 255]);
|
|
let mut hero_build = hero_comp.clone();
|
|
// deterministic lcg(11)
|
|
let mut s: u32 = 11;
|
|
let mut next = || {
|
|
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
|
|
s as f64 / 0xffffffffu32 as f64
|
|
};
|
|
for y in 20..60usize {
|
|
for x in 20..480usize {
|
|
let v = (next() * 255.0).floor() as u8;
|
|
let p = (y * 512 + x) * 4;
|
|
hero_build.data[p] = v;
|
|
hero_build.data[p + 1] = v;
|
|
hero_build.data[p + 2] = v;
|
|
}
|
|
}
|
|
let inv = hero::invented_ink(&hero_comp, &hero_build);
|
|
assert_json_eq(&inv["cells"], &gh["invented"]["cells"], "invented.cells");
|
|
approx(f(&inv["fraction"]), &gh["invented"]["fraction"], 1e-9, "invented.fraction");
|
|
|
|
// plateClip
|
|
let mut plate_comp = r::create_image(300, 200, [255, 255, 255, 255]);
|
|
r::fill_rect(&mut plate_comp, 40.0, 30.0, 200.0, 140.0, rgb([10, 20, 40]));
|
|
let mut plate_build = r::create_image(300, 200, [255, 255, 255, 255]);
|
|
r::fill_rect(&mut plate_build, 0.0, 0.0, 260.0, 180.0, rgb([10, 20, 40]));
|
|
let pc = hero::plate_clip_check(®ion, &plate_comp, &plate_build);
|
|
assert_json_eq(&pc, &gh["plateClip"], "plateClip");
|
|
|
|
// chromeStrip
|
|
let mut strip_comp = r::create_image(600, 120, [255, 255, 255, 255]);
|
|
r::fill_rect(&mut strip_comp, 0.0, 0.0, 600.0, 40.0, rgb([20, 30, 50]));
|
|
r::fill_rect(&mut strip_comp, 0.0, 58.0, 600.0, 2.0, rgb([0, 0, 0]));
|
|
let mut strip_build = r::create_image(600, 120, [255, 255, 255, 255]);
|
|
r::fill_rect(&mut strip_build, 0.0, 0.0, 600.0, 40.0, rgb([20, 30, 50]));
|
|
r::fill_rect(&mut strip_build, 0.0, 88.0, 600.0, 2.0, rgb([0, 0, 0]));
|
|
let nav = hero::Region { id: "nav".into(), kind: "band".into(), chosen: None };
|
|
let cs = hero::chrome_strip_check(&nav, &strip_comp, &strip_build);
|
|
assert_json_eq(&cs["findings"], &gh["chromeStrip"]["findings"], "chromeStrip.findings");
|
|
assert_json_eq(&cs["comp"], &gh["chromeStrip"]["comp"], "chromeStrip.comp");
|
|
assert_json_eq(&cs["build"], &gh["chromeStrip"]["build"], "chromeStrip.build");
|
|
|
|
// ruleRows
|
|
let rr: Vec<Value> = hero::rule_rows(&strip_comp, 0.5, 28.0).into_iter().map(|v| Value::from(v)).collect();
|
|
assert_json_eq(&Value::Array(rr), &gh["ruleRows"]["strip"], "ruleRows");
|
|
|
|
// inkColor of comp crop
|
|
let ic = hero::ink_color(&comp_crop);
|
|
match (ic.and_then(|c| c.ink), gh["inkColor"].is_null()) {
|
|
(Some(ink), false) => assert_eq!(ink.hex, gh["inkColor"]["hex"].as_str().unwrap(), "inkColor.hex"),
|
|
(None, true) => {}
|
|
_ => panic!("inkColor mismatch"),
|
|
}
|
|
|
|
// svgIllustrations
|
|
let html = "\n<svg width=\"24\" height=\"24\"><path d=\"M1 1 L2 2\"/></svg>\n<svg viewBox=\"0 0 800 600\"><path d=\"".to_string()
|
|
+ &"M0 0 ".repeat(200)
|
|
+ "\"/><path d=\""
|
|
+ &"L1 1 ".repeat(100)
|
|
+ "\"/><polyline points=\""
|
|
+ &"1,2 ".repeat(50)
|
|
+ "\"/></svg>\n<svg width=\"48\" height=\"48\"><use href=\"#icon\"/></svg>\n";
|
|
let svg = hero::svg_illustrations(&html);
|
|
let gs = gh["svg"].as_array().unwrap();
|
|
assert_eq!(svg.len(), gs.len(), "svg count");
|
|
for (s, gsv) in svg.iter().zip(gs) {
|
|
assert_eq!(s["paths"].as_i64(), gsv["paths"].as_i64(), "svg paths");
|
|
assert_eq!(s["budget"].as_i64(), gsv["budget"].as_i64(), "svg budget");
|
|
approx(f(&s["long"]), &gsv["long"], 1e-9, "svg long");
|
|
assert_eq!(s["label"], gsv["label"], "svg label");
|
|
}
|
|
}
|