use anyd::GrayImage;
use anyd::codes::qr::{EcLevel, QrEncoder, scan};
use anyd::output::Encoding;
use anyd::render::render_matrix;
use anyd::segment::Segment;
use anyd::traits::Encode;
use anyd::transform::{self, Axis, Rng};
const SCALE: usize = 6;
fn encode(segments: Vec<Segment>, level: EcLevel) -> (GrayImage, Vec<u8>, usize) {
let enc = QrEncoder::new();
let symbol = enc.build(segments, level).expect("in-capacity payload");
let payload = symbol.payload_bytes();
let matrix = match enc.encode(&symbol).expect("encode") {
Encoding::Matrix(m) => m,
Encoding::Linear(_) => unreachable!("QR encodes to a matrix"),
};
let dim = matrix.width();
(render_matrix(&matrix, SCALE), payload, dim)
}
fn lighting_gradient(img: &GrayImage, lo: f32, hi: f32) -> GrayImage {
let (w, h) = (img.width(), img.height());
let mut out = GrayImage::new(w, h);
for y in 0..h {
for x in 0..w {
let t = x as f32 / (w.max(2) - 1) as f32;
let g = lo + (hi - lo) * t;
let v = (img.get(x, y) as f32 * g).round().clamp(0.0, 255.0) as u8;
out.set(x, y, v);
}
}
out
}
fn assert_decodes(img: &GrayImage, expected: &[u8], label: &str) {
match scan(&img.as_frame()) {
Ok(sym) => assert_eq!(
sym.payload_bytes(),
expected,
"{label}: payload mismatch after real-world degradation"
),
Err(e) => panic!("{label}: sampler failed to decode ({e})"),
}
}
fn cases() -> Vec<(Vec<Segment>, EcLevel)> {
vec![
(vec![Segment::byte(b"HI".to_vec())], EcLevel::M), (
vec![Segment::alphanumeric(b"ABCDEFGH IJKLMN".to_vec())],
EcLevel::M,
), (
vec![Segment::byte(b"https://example.com/x?q=hello".to_vec())],
EcLevel::Q,
), (
vec![Segment::byte(b"MIXED payload 2026 test-suite".to_vec())],
EcLevel::H,
), (vec![Segment::byte(vec![b'B'; 120])], EcLevel::M), ]
}
#[test]
fn uneven_lighting_gradient() {
for (payload, level) in cases() {
let (base, expected, dim) = encode(payload, level);
let dark_right = lighting_gradient(&base, 1.0, 0.4);
assert_decodes(
&dark_right,
&expected,
&format!("dim{dim} gradient→dark-right"),
);
let dark_left = lighting_gradient(&base, 0.4, 1.0);
assert_decodes(
&dark_left,
&expected,
&format!("dim{dim} gradient→dark-left"),
);
}
}
#[test]
fn lighting_gradient_with_blur() {
for (payload, level) in cases() {
let (base, expected, dim) = encode(payload, level);
let img = transform::gaussian_blur(&lighting_gradient(&base, 1.0, 0.45), 1.5);
assert_decodes(&img, &expected, &format!("dim{dim} gradient+blur"));
}
}
#[test]
fn combined_capture_degradations() {
for (payload, level) in cases() {
let (base, expected, dim) = encode(payload, level);
let mut img = lighting_gradient(&base, 1.0, 0.5);
img = transform::rotate(&img, 6.0f32.to_radians());
img = transform::gaussian_blur(&img, 1.2);
let mut rng = Rng::new(7);
img = transform::add_noise(&img, 12.0, &mut rng);
assert_decodes(&img, &expected, &format!("dim{dim} combined-capture"));
}
}
#[test]
fn tilt_and_rotation_together() {
for (payload, level) in cases() {
let (base, expected, dim) = encode(payload, level);
if dim < 25 {
continue;
}
let mut img = transform::rotate(&base, 7.0f32.to_radians());
img = transform::tilt_right(&img, 0.12);
assert_decodes(&img, &expected, &format!("dim{dim} tilt+rotation"));
}
}
#[test]
fn cylinder_curvature() {
for (payload, level) in cases() {
let (base, expected, dim) = encode(payload, level);
let curved = transform::cylinder(&base, 0.45, Axis::Vertical);
assert_decodes(&curved, &expected, &format!("dim{dim} cylinder0.45"));
if dim >= 25 {
let curved_blur =
transform::gaussian_blur(&transform::cylinder(&base, 0.35, Axis::Vertical), 1.0);
assert_decodes(&curved_blur, &expected, &format!("dim{dim} cylinder+blur"));
}
}
}
fn mesh_cases() -> Vec<(Vec<Segment>, EcLevel)> {
vec![
(vec![Segment::byte(vec![b'B'; 120])], EcLevel::M), (vec![Segment::byte(vec![b'C'; 220])], EcLevel::M), (vec![Segment::byte(vec![b'D'; 440])], EcLevel::M), ]
}
#[test]
fn strong_cylinder_beyond_flat_envelope() {
let (base2, exp2, _) = encode(vec![Segment::byte(vec![b'A'; 20])], EcLevel::M); for &c in &[0.6f32, 0.8] {
let curved = transform::cylinder(&base2, c, Axis::Vertical);
assert_decodes(&curved, &exp2, &format!("dim25 cylinder{c}"));
}
let cb = transform::gaussian_blur(&transform::cylinder(&base2, 0.7, Axis::Vertical), 1.0);
assert_decodes(&cb, &exp2, "dim25 cylinder0.7+blur");
let (base7, exp7, _) = encode(vec![Segment::byte(vec![b'B'; 120])], EcLevel::M); let curved7 = transform::cylinder(&base7, 0.6, Axis::Vertical);
assert_decodes(&curved7, &exp7, "dim45 cylinder0.6");
}
#[test]
fn nonplanar_ripple_and_fold_dewarp() {
for (payload, level) in mesh_cases() {
let (base, expected, dim) = encode(payload, level);
let wl = (dim * SCALE) as f32 * 1.5;
let waved = transform::wave(&base, 10.0, wl, Axis::Vertical);
assert_decodes(&waved, &expected, &format!("dim{dim} ripple"));
}
let (base7, exp7, _) = encode(vec![Segment::byte(vec![b'B'; 120])], EcLevel::M); let folded7 = transform::fold(&base7, 0.5, 40.0, Axis::Vertical);
assert_decodes(&folded7, &exp7, "dim45 fold40");
let (base16, exp16, _) = encode(vec![Segment::byte(vec![b'D'; 440])], EcLevel::M); let folded16 = transform::fold(&base16, 0.5, 25.0, Axis::Vertical);
assert_decodes(&folded16, &exp16, "dim81 fold25");
}
#[cfg(feature = "cli")]
#[test]
fn real_bottle_photo_front_end() {
use anyd::GrayFrame;
use anyd::codes::qr::{QrScanner, sample_grid};
use anyd::pipeline::Hints;
use anyd::traits::Detect;
let bytes = std::fs::read("testdata/real_qr_bottle.png").expect("read fixture");
let rgba = oxideav_png::decode_png_to_rgba(&bytes).expect("decode PNG");
let (w, h) = (rgba.width as usize, rgba.height as usize);
let luma: Vec<u8> = rgba
.data
.chunks_exact(4)
.map(|p| ((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114) / 1000) as u8)
.collect();
let frame = GrayFrame::new(&luma, w, h).expect("valid frame");
let candidates = QrScanner::new().detect(&frame, &Hints::new());
assert_eq!(candidates.len(), 1, "front-end should locate the bottle QR");
let module_size = candidates[0]
.location
.module_size
.expect("candidate carries a module size");
assert!(
(2.0..8.0).contains(&module_size),
"module size {module_size} implausible for the fixture"
);
let grid = sample_grid(&frame).expect("front-end recovers a module grid");
assert_eq!(grid.width(), grid.height(), "recovered grid must be square");
assert!(
grid.width() >= 21 && (grid.width() - 21).is_multiple_of(4),
"recovered dimension {} is not a valid QR size",
grid.width()
);
assert_eq!(grid.width(), 25, "fixture is a version-2 (25×25) symbol");
let _ = scan(&frame);
}
#[cfg(feature = "cli")]
#[test]
fn real_scene_locate_then_decode_ean() {
use anyd::GrayFrame;
use anyd::detect::{LocateOptions, locate};
let bytes = std::fs::read("testdata/real_scene_ean.png").expect("read fixture");
let rgba = oxideav_png::decode_png_to_rgba(&bytes).expect("decode PNG");
let (w, h) = (rgba.width as usize, rgba.height as usize);
let luma: Vec<u8> = rgba
.data
.chunks_exact(4)
.map(|p| ((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114) / 1000) as u8)
.collect();
let frame = GrayFrame::new(&luma, w, h).expect("valid frame");
let bounds = |c: &anyd::pipeline::Candidate| {
let cs = c.location.outline.corners;
let x0 = cs.iter().map(|p| p.x).fold(f32::MAX, f32::min).max(0.0) as usize;
let y0 = cs.iter().map(|p| p.y).fold(f32::MAX, f32::min).max(0.0) as usize;
let x1 = (cs.iter().map(|p| p.x).fold(0.0, f32::max) as usize).min(w);
let y1 = (cs.iter().map(|p| p.y).fold(0.0, f32::max) as usize).min(h);
(x0, y0, x1, y1)
};
let (bx, by) = (512usize, 295usize);
let cands = locate(&frame, &LocateOptions::default());
let bar = cands
.iter()
.find(|c| {
c.symbology.map(|s| s.dimension()) == Some(anyd::Dimension::Linear) && {
let (x0, y0, x1, y1) = bounds(c);
x0 <= bx && bx < x1 && y0 <= by && by < y1
}
})
.expect("locate() must isolate the barcode as a linear candidate over its centre");
let (x0, y0, x1, y1) = bounds(bar);
let (cw, ch) = (x1 - x0, y1 - y0);
let mut crop = vec![0u8; cw * ch];
for y in 0..ch {
for x in 0..cw {
crop[y * cw + x] = frame.get_unchecked(x0 + x, y0 + y);
}
}
let cframe = GrayFrame::new(&crop, cw, ch).expect("valid crop");
let lines = anyd::scan1d::scan_lines(&cframe, &anyd::scan1d::ScanOptions::default());
let dec = anyd::codes::ean::EanDecoder::new();
let text = lines
.iter()
.find_map(|cand| anyd::scan1d::try_decode(cand, &dec).and_then(|s| s.text()))
.expect("the located region must decode as EAN-13");
assert_eq!(
text, "4901085663356",
"wrong payload from the located barcode"
);
}
#[cfg(feature = "cli")]
#[test]
fn real_scene_locator_precision() {
use anyd::GrayFrame;
use anyd::detect::{LocateOptions, locate};
let load = |path: &str| {
let bytes = std::fs::read(path).expect("read fixture");
let rgba = oxideav_png::decode_png_to_rgba(&bytes).expect("decode PNG");
let (w, h) = (rgba.width as usize, rgba.height as usize);
let luma: Vec<u8> = rgba
.data
.chunks_exact(4)
.map(|p| ((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114) / 1000) as u8)
.collect();
(luma, w, h)
};
let bbox = |c: &anyd::pipeline::Candidate| {
let cs = c.location.outline.corners;
(
cs.iter().map(|p| p.x).fold(f32::MAX, f32::min),
cs.iter().map(|p| p.y).fold(f32::MAX, f32::min),
cs.iter().map(|p| p.x).fold(f32::MIN, f32::max),
cs.iter().map(|p| p.y).fold(f32::MIN, f32::max),
)
};
let (luma, w, h) = load("testdata/real_scene_ean.png");
let frame = GrayFrame::new(&luma, w, h).unwrap();
let cands = locate(&frame, &LocateOptions::default());
assert!(
(1..=6).contains(&cands.len()),
"EAN scene: expected a handful of candidates, got {}",
cands.len()
);
let first = bbox(&cands[0]);
assert_eq!(
cands[0].symbology.map(|s| s.dimension()),
Some(anyd::Dimension::Linear),
"EAN scene: strongest candidate should be the linear barcode"
);
assert!(
first.0 <= 512.0 && 512.0 < first.2 && first.1 <= 295.0 && 295.0 < first.3,
"EAN scene: strongest candidate {first:?} does not cover the barcode centre"
);
for c in &cands {
let (x0, y0, x1, y1) = bbox(c);
assert!(
(x1 - x0) * (y1 - y0) <= 0.7 * (w * h) as f32,
"EAN scene: scene-sized blob ({x0},{y0})-({x1},{y1}) not capped"
);
}
let (luma, w, h) = load("testdata/real_scene_qr.png");
let frame = GrayFrame::new(&luma, w, h).unwrap();
let cands = locate(&frame, &LocateOptions::default());
assert_eq!(
cands.len(),
1,
"close-up QR scene should collapse to a single candidate"
);
let (x0, y0, x1, y1) = bbox(&cands[0]);
assert!(
x0 <= 152.0 && 152.0 < x1 && y0 <= 152.0 && 152.0 < y1,
"QR scene: candidate ({x0},{y0})-({x1},{y1}) misses the symbol centre"
);
assert_eq!(
cands[0].symbology.map(|s| s.dimension()),
Some(anyd::Dimension::Matrix),
);
let (luma, w, h) = load("testdata/real_scene_text.png");
let frame = GrayFrame::new(&luma, w, h).unwrap();
let cands = locate(&frame, &LocateOptions::default());
for c in &cands {
let (x0, y0, x1, y1) = bbox(c);
let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
assert!(
cx >= 150.0 && cy >= 100.0,
"text scene: candidate centred at ({cx},{cy}) sits on plain text"
);
}
}
#[cfg(feature = "cli")]
#[test]
fn real_can_qr_curved_blurred_decode() {
use anyd::GrayFrame;
let bytes = std::fs::read("testdata/real_qr_can.png").expect("read fixture");
let rgba = oxideav_png::decode_png_to_rgba(&bytes).expect("decode PNG");
let (w, h) = (rgba.width as usize, rgba.height as usize);
let luma: Vec<u8> = rgba
.data
.chunks_exact(4)
.map(|p| ((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114) / 1000) as u8)
.collect();
let frame = GrayFrame::new(&luma, w, h).expect("valid frame");
let sym = scan(&frame).expect("curved blurred can QR must decode");
assert_eq!(sym.text().as_deref(), Some("https://zko.jp/ueacvj"));
}
#[cfg(feature = "cli")]
#[test]
fn real_scene_curved_ean_edge_decode() {
use anyd::GrayFrame;
use anyd::scan1d::ScanOptions;
let bytes = std::fs::read("testdata/real_scene_ean_curved.png").expect("read fixture");
let rgba = oxideav_png::decode_png_to_rgba(&bytes).expect("decode PNG");
let (w, h) = (rgba.width as usize, rgba.height as usize);
let luma: Vec<u8> = rgba
.data
.chunks_exact(4)
.map(|p| ((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114) / 1000) as u8)
.collect();
let frame = GrayFrame::new(&luma, w, h).expect("valid frame");
let sym = anyd::codes::ean::scan(&frame, &ScanOptions::default())
.expect("curved bottle EAN-13 must decode via the width-ratio edge path");
assert_eq!(
sym.text().as_deref(),
Some("4901085663356"),
"wrong payload from the curved-bottle capture"
);
assert_eq!(sym.symbology, anyd::Symbology::Ean13);
}
#[cfg(feature = "cli")]
#[test]
fn real_scene_text_is_not_a_barcode() {
use anyd::GrayFrame;
use anyd::scan1d::ScanOptions;
let bytes = std::fs::read("testdata/real_scene_text.png").expect("read fixture");
let rgba = oxideav_png::decode_png_to_rgba(&bytes).expect("decode PNG");
let (w, h) = (rgba.width as usize, rgba.height as usize);
let luma: Vec<u8> = rgba
.data
.chunks_exact(4)
.map(|p| ((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114) / 1000) as u8)
.collect();
let frame = GrayFrame::new(&luma, w, h).expect("valid frame");
assert!(
anyd::codes::ean::scan(&frame, &ScanOptions::default()).is_none(),
"consensus floor must reject stray checksum-passing misreads on plain text"
);
}
#[cfg(feature = "cli")]
#[test]
fn real_scene_qr_via_scan_all() {
use anyd::GrayFrame;
let bytes = std::fs::read("testdata/real_scene_qr.png").expect("read fixture");
let rgba = oxideav_png::decode_png_to_rgba(&bytes).expect("decode PNG");
let (w, h) = (rgba.width as usize, rgba.height as usize);
let luma: Vec<u8> = rgba
.data
.chunks_exact(4)
.map(|p| ((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114) / 1000) as u8)
.collect();
let frame = GrayFrame::new(&luma, w, h).expect("valid frame");
let found = anyd::pipeline::scan_all(&frame);
let qr = found
.iter()
.find(|s| s.symbology == anyd::Symbology::QrCode)
.expect("scan_all must read the QR capture");
assert_eq!(
qr.text().as_deref(),
Some(
"https://www.omronconnect.com/devices/?utm_source=qrcode&utm_medium=package&utm_campaign=mc-6800b"
),
);
}