wai-rs: prove the reconstruction binding over a real jpegai decode #53

Merged
dcharlot merged 1 commit from wai-binding-real-decode into main 2026-08-25 17:10:00 -04:00

View file

@ -0,0 +1,96 @@
//! End-to-end: a **real** byte-exact JPEG-AI decode → a reproducible-reconstruction
//! binding over the decoded pixels. The [`wai::reconstruction_binding`] unit tests cover
//! the binding on synthetic RGB; this closes the loop on real content — the byte-exact
//! decoder produces the pixels, and the C2PA/JPEG-Trust hard binding is taken over them.
//!
//! It exercises the property the whole capability rests on: the decode is deterministic
//! (`decode-equivalence`), so the binding taken over one decode verifies any faithful
//! reproduction, and a single flipped pixel breaks it. That is exactly what a hard
//! binding over neural-coded media could not do before a byte-exact decode existed.
//!
//! env: WAI_JPEGAI_CODESTREAM, WAI_JPEGAI_WEIGHTS_DIR, WAI_JPEGAI_NORM
//! (the bundle from `tools/wai_jpegai_build_e2e_inputs.py`; see jpegai_e2e_full).
#![cfg(all(feature = "neural_int", feature = "provenance"))]
use wai::jpegai_decode::{decode, DecoderWeights, NormTables, PlaneWeights};
use wai::jpegai_icci::IcciNet;
use wai::jpegai_synthesis::{IntNet, Weights};
use wai::reconstruction_binding::{ReconstructionBinding, ReconstructionDescriptor};
fn jload(p: &std::path::Path) -> serde_json::Value {
serde_json::from_slice(&std::fs::read(p).unwrap()).unwrap()
}
#[test]
fn real_decode_to_reconstruction_binding() {
let (Ok(cs), Ok(dir), Ok(nf)) = (
std::env::var("WAI_JPEGAI_CODESTREAM"),
std::env::var("WAI_JPEGAI_WEIGHTS_DIR"),
std::env::var("WAI_JPEGAI_NORM"),
) else {
eprintln!("skip: set WAI_JPEGAI_CODESTREAM + WAI_JPEGAI_WEIGHTS_DIR + WAI_JPEGAI_NORM");
return;
};
let Ok(data) = std::fs::read(&cs) else { eprintln!("skip: no codestream"); return };
// --- load the model bundle (same shape as jpegai_e2e_full) ---
let dpath = std::path::Path::new(&dir);
let w = |n: &str| Weights::from_json(&jload(&dpath.join(n)));
let net = |n: &str| IntNet::from_json(&jload(&dpath.join(n)));
let weights = DecoderWeights {
luma: PlaneWeights { hyper_scale: net("hs_y.json"), hyper_decoder: w("hyp_y.json"), synthesis: w("synth_y.json") },
chroma: PlaneWeights { hyper_scale: net("hs_uv.json"), hyper_decoder: w("hyp_uv.json"), synthesis: w("synth_uv.json") },
mcm_y: [w("mcm0_y.json"), w("mcm1_y.json"), w("mcm2_y.json"), w("mcm3_y.json")],
icci: IcciNet::from_json(&jload(&dpath.join("icci.json"))),
};
let nv = jload(std::path::Path::new(&nf));
let i64v = |k: &str| nv[k].as_array().unwrap().iter().map(|x| x.as_i64().unwrap()).collect::<Vec<i64>>();
let u8v = |k: &str| nv[k].as_array().unwrap().iter().map(|x| x.as_u64().unwrap() as u8).collect::<Vec<u8>>();
let i32v = |k: &str| nv[k].as_array().unwrap().iter().map(|x| x.as_i64().unwrap() as i32).collect::<Vec<i32>>();
let i3 = |k: &str| { let a = nv[k].as_array().unwrap(); [a[0].as_i64().unwrap(), a[1].as_i64().unwrap(), a[2].as_i64().unwrap()] };
let f3 = |k: &str| { let a = nv[k].as_array().unwrap(); [a[0].as_f64().unwrap() as f32, a[1].as_f64().unwrap() as f32, a[2].as_f64().unwrap() as f32] };
let norm = NormTables {
pmf: nv["pmf"].as_array().unwrap().iter()
.map(|r| r.as_array().unwrap().iter().map(|x| x.as_i64().unwrap()).collect()).collect(),
bounds: u8v("bounds"), per_channel: nv["per_channel"].as_u64().unwrap() as usize,
cdfs_y: u8v("cdfs_y"), cdfs_uv: u8v("cdfs_uv"), gain_y: i64v("gain_y"), gain_uv: i64v("gain_uv"),
lsbs_t0: i32v("lsbs_t0"), lsbs_t1: i32v("lsbs_t1"), lef_thr: i3("lef_thr0"), lef_mag: f3("lef_mag0"),
};
// --- decode the real codestream → RGB, then bind over the decoded pixels ---
let img = decode(&data, &weights, &norm).expect("supported geometry");
assert_eq!((img.h, img.w), (256, 256));
assert_eq!(img.data.len(), 256 * 256 * 3);
let desc = ReconstructionDescriptor {
decoder_id: 0, // WAI pins one synthesis branch
profile: "tools_0".into(),
level: "L1".into(),
width: img.w as u32,
height: img.h as u32,
format: "RGB8".into(),
color_space: "bt709".into(),
};
let binding = ReconstructionBinding::bind(&img.data, desc.clone());
assert_eq!(binding.alg, "sha256");
assert_eq!(binding.hash_hex().len(), 64);
// The binding verifies its own decoded pixels.
assert!(binding.verify(&img.data), "the binding must verify the pixels it was taken over");
// decode-equivalence: a second decode is byte-identical, and the binding — taken
// over the first — verifies the second. That is the portable-hard-binding property.
let img2 = decode(&data, &weights, &norm).expect("supported geometry");
assert_eq!(img2.data, img.data, "the decode is deterministic (decode-equivalence)");
assert!(binding.verify(&img2.data), "the binding verifies any faithful reproduction");
// A single flipped pixel breaks the hard binding.
let mut tampered = img.data.clone();
tampered[12_345] ^= 1;
assert!(!binding.verify(&tampered), "a single flipped byte must fail the binding");
eprintln!("reconstruction binding over the real JPEG-AI decode:");
eprintln!(" assertion = {}", ReconstructionBinding::C2PA_LABEL);
eprintln!(" sha256 = {}", binding.hash_hex());
eprintln!(" json = {}", binding.to_json());
}