use anyd::GrayImage;
use anyd::codes::qr::{EcLevel, QrEncoder, QrScanner, sample_grid, scan};
use anyd::output::{BitMatrix, Encoding};
use anyd::pipeline::Hints;
use anyd::render::render_matrix;
use anyd::segment::Segment;
use anyd::traits::{Analyze, Detect, Encode};
use anyd::transform::{self, Rng};
const SCALE: usize = 6;
fn encode(segments: Vec<Segment>, level: EcLevel) -> (BitMatrix, Vec<u8>) {
let enc = QrEncoder::new();
let symbol = enc.build(segments, level).expect("in-capacity payload");
let payload = symbol.payload_bytes();
match enc.encode(&symbol).expect("encode") {
Encoding::Matrix(m) => (m, payload),
Encoding::Linear(_) => unreachable!("QR encodes to a matrix"),
}
}
fn assert_decodes(img: &GrayImage, expected: &[u8], label: &str) {
let frame = img.as_frame();
match scan(&frame) {
Ok(sym) => assert_eq!(
sym.payload_bytes(),
expected,
"{label}: payload mismatch after image round-trip"
),
Err(e) => panic!("{label}: sampler failed to decode ({e})"),
}
}
fn base_image(matrix: &BitMatrix) -> (GrayImage, usize) {
(render_matrix(matrix, SCALE), matrix.width())
}
#[test]
fn upright_is_pixel_exact() {
for payload in ["HI", "https://example.com/x?q=hello", "MIXED 2026 test"] {
let (matrix, _) = encode(vec![Segment::byte(payload.as_bytes().to_vec())], EcLevel::Q);
let (img, _) = base_image(&matrix);
let sampled = sample_grid(&img.as_frame()).expect("sample upright");
assert_eq!(
sampled, matrix,
"{payload}: upright sampling not pixel-exact"
);
}
}
#[test]
fn rotations() {
for (payload, level) in cases() {
let (matrix, expected) = encode(payload, level);
let (base, dim) = base_image(&matrix);
for deg in [5.0f32, -5.0, 15.0, -15.0, 30.0, -30.0, 45.0, -45.0, 90.0] {
let img = transform::rotate(&base, deg.to_radians());
assert_decodes(&img, &expected, &format!("dim{dim} rot{deg}"));
}
if dim <= 29 {
let img = transform::rotate(&base, std::f32::consts::PI);
assert_decodes(&img, &expected, &format!("dim{dim} rot180"));
}
}
}
#[test]
fn scaling() {
for (payload, level) in cases() {
let (matrix, expected) = encode(payload, level);
let (base, dim) = base_image(&matrix);
for factor in [1.5f32, 2.0] {
let img = transform::scale(&base, factor);
assert_decodes(&img, &expected, &format!("dim{dim} scale{factor}"));
}
if dim <= 33 {
let img = transform::scale(&base, 0.75);
assert_decodes(&img, &expected, &format!("dim{dim} scale0.75"));
}
}
}
#[test]
fn blur() {
for (payload, level) in cases() {
let (matrix, expected) = encode(payload, level);
let (base, dim) = base_image(&matrix);
for sigma in [1.0f32, 1.5, 2.0] {
let img = transform::gaussian_blur(&base, sigma);
assert_decodes(&img, &expected, &format!("dim{dim} blur{sigma}"));
}
}
}
#[test]
fn noise() {
for (payload, level) in cases() {
let (matrix, expected) = encode(payload, level);
let (base, dim) = base_image(&matrix);
for sigma in [15.0f32, 25.0] {
let mut rng = Rng::new(0xC0FFEE);
let img = transform::add_noise(&base, sigma, &mut rng);
assert_decodes(&img, &expected, &format!("dim{dim} noise{sigma}"));
}
}
}
#[test]
fn brightness_and_contrast() {
for (payload, level) in cases() {
let (matrix, expected) = encode(payload, level);
let (base, dim) = base_image(&matrix);
for (bright, contrast) in [(-40.0f32, 1.0f32), (40.0, 1.0), (0.0, 0.6), (0.0, 1.4)] {
let img = transform::brightness_contrast(&base, bright, contrast);
assert_decodes(&img, &expected, &format!("dim{dim} bc{bright}/{contrast}"));
}
}
}
#[test]
fn perspective_tilt() {
for (payload, level) in cases() {
let (matrix, expected) = encode(payload, level);
let (base, dim) = base_image(&matrix);
if !(25..=45).contains(&dim) {
continue;
}
let r = transform::tilt_right(&base, 0.15);
assert_decodes(&r, &expected, &format!("dim{dim} tiltR0.15"));
let b = transform::tilt_bottom(&base, 0.15);
assert_decodes(&b, &expected, &format!("dim{dim} tiltB0.15"));
if dim <= 33 {
let r = transform::tilt_right(&base, 0.2);
assert_decodes(&r, &expected, &format!("dim{dim} tiltR0.2"));
let b = transform::tilt_bottom(&base, 0.2);
assert_decodes(&b, &expected, &format!("dim{dim} tiltB0.2"));
}
}
}
#[test]
fn combined_pipeline() {
for (payload, level) in cases() {
let (matrix, expected) = encode(payload, level);
let (base, dim) = base_image(&matrix);
let mut rng = Rng::new(1234);
let rotated = transform::rotate(&base, 8.0f32.to_radians());
let blurred = transform::gaussian_blur(&rotated, 1.0);
let noised = transform::add_noise(&blurred, 12.0, &mut rng);
assert_decodes(&noised, &expected, &format!("dim{dim} combo"));
}
}
#[test]
fn detect_and_analyze_traits() {
let (matrix, expected) = encode(vec![Segment::byte(b"PIPELINE 2026".to_vec())], EcLevel::Q);
let (base, _) = base_image(&matrix);
let rotated = transform::rotate(&base, 12.0f32.to_radians());
let frame = rotated.as_frame();
let scanner = QrScanner::new();
let candidates = scanner.detect(&frame, &Hints::new());
assert_eq!(candidates.len(), 1, "detector should find one QR candidate");
let cand = &candidates[0];
assert_eq!(cand.symbology, Some(anyd::Symbology::QrCode));
assert!(cand.location.module_size.unwrap() > 0.0);
let symbol = scanner.analyze(&frame, cand).expect("analyze");
assert_eq!(symbol.payload_bytes(), expected);
assert_eq!(symbol.symbology, anyd::Symbology::QrCode);
}
#[test]
fn known_hard_edge_cases() {
let (matrix, expected) = encode(vec![Segment::byte(vec![b'Q'; 40])], EcLevel::L);
let (base, dim) = base_image(&matrix);
assert!((25..=33).contains(&dim), "expected v2..v4, got dim {dim}");
let steep = transform::tilt_right(&base, 0.25);
assert_decodes(&steep, &expected, "known-hard steep-tilt");
let mut rng = Rng::new(555);
let rot = transform::rotate(&base, 22.0f32.to_radians());
let noisy = transform::add_noise(&rot, 22.0, &mut rng);
assert_decodes(&noisy, &expected, "known-hard rot+noise");
let (m2, exp2) = encode(vec![Segment::alphanumeric(b"EDGE 42".to_vec())], EcLevel::H);
let (b2, _) = base_image(&m2);
let small = transform::scale(&b2, 0.7);
assert_decodes(&small, &exp2, "known-hard downscale");
}
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), ]
}