anyd 0.1.1

From-scratch encoding and decoding of 1D and 2D barcodes with lossless round-trip and live-video detection
Documentation
//! End-to-end MicroPDF417 (ISO/IEC 24728) tests.
//!
//! Two things are checked:
//!
//! 1. **Independent reference vectors.** The expected module bitmaps are generated by
//!    the (BSD-3-Clause) zint project, version 2.16.0, via
//!    `zint -b MICROPDF417 -d <DATA> --cols <N> --scale 1 -o out.svg`, reading the dark
//!    modules straight out of zint's rendered SVG vector path (the SVG is used rather
//!    than `--dump` because `--dump`'s byte-packing leaves the trailing stop module of
//!    a non-byte-aligned row unreliable). zint's MicroPDF417 output is itself
//!    cross-checked against BWIPP in its own test suite, so this pins our encoder to an
//!    external implementation of the standard across all four column configurations.
//!
//! 2. **Lossless round-trip.** `encode → decode → re-encode` must reproduce the exact
//!    segments, metadata and module matrix across Text/Byte/Numeric compaction and
//!    every column count.

use anyd::codes::pdf417::{MicroPdf417Decoder, MicroPdf417Encoder};
use anyd::output::{BitMatrix, Encoding};
use anyd::segment::Segment;
use anyd::traits::{Decode, Encode};

/// Reference bitmaps from zint 2.16.0. Each entry is `(data, columns, rows)` where
/// every row string is `width` characters of `0`/`1`, most-significant module first.
#[allow(clippy::type_complexity)]
const REFERENCE: &[(&str, usize, &[&str])] = &[
    // zint 2.16.0 SVG:  zint -b MICROPDF417 -d "A" --cols 1  (11 rows x 38)
    (
        "A",
        1,
        &[
            "11001000101000011000110010011100110101",
            "11101000101111101011101111011110110101",
            "11101100101100011111001001011110010101",
            "11001100101000011000110010011100010101",
            "11011100101111101000101100011000010101",
            "11011110101100010010111110011000110101",
            "11001110101001111011110100011000100101",
            "11101110101111011001100100011100100101",
            "11100110101001111110011101011110100101",
            "11110110101110001010111000011110101101",
            "11110010101011111100010011011110101001",
        ],
    ),
    // zint 2.16.0 SVG:  zint -b MICROPDF417 -d "ABCDEFGH" --cols 2  (8 rows x 55)
    (
        "ABCDEFGH",
        2,
        &[
            "1100100010100001100011001001111010101111000011001000101",
            "1110100010110101111110111101111101000100110011101000101",
            "1110110010101101100111100001100011111001001011101100101",
            "1100110010100001100011001001000011000110010011001100101",
            "1101110010100011011111101101111001000001001011011100101",
            "1101111010110011110100011001000111100001010011011110101",
            "1100111010100000101100110001110111001000001011001110101",
            "1110111010111110011000101001110100110100000011101110101",
        ],
    ),
    // zint 2.16.0 SVG:  zint -b MICROPDF417 -d "Wikipedia" --cols 3  (6 rows x 82)
    (
        "Wikipedia",
        3,
        &[
            "1100100010100001100011001001011001110110011101110011001110010001001110011001000101",
            "1110100010110010111100001101001001110111111010001011101100101110001000011101000101",
            "1110110010110000111010111001001101110101100001001110001000010001001111011101100101",
            "1100110010110000100010001101000101110110011010000010001001100111011000011001100101",
            "1101110010100010000111101001000100110101111101100111101101101111000001011011100101",
            "1101111010100011010001110001000110110101110000100011001010100000111100011011110101",
        ],
    ),
    // zint 2.16.0 SVG:  zint -b MICROPDF417 -d "123456789012" --cols 4  (4 rows x 99)
    (
        "123456789012",
        4,
        &[
            "110100111010111111010011000111111010101110001001110110100110001111101001111101001100111011010010001",
            "110100110010000010101111000101000010011110001001110100110001111100100101100011111001001011010110001",
            "110100010011100011000100110101110111101100001001100100100011001111011101000111011110110011010111001",
            "110100011011100010110000010111100101000000101001100110111111001110010101111110111000101011010111101",
        ],
    ),
];

/// Render a symbol's matrix, sampling the top module of each stacked codeword row into
/// a bit string per row (the encoder renders every codeword row several modules tall).
fn row_strings(matrix: &BitMatrix, num_rows: usize) -> Vec<String> {
    let step = matrix.height() / num_rows;
    (0..num_rows)
        .map(|r| {
            let y = r * step;
            (0..matrix.width())
                .map(|x| if matrix.get(x, y) { '1' } else { '0' })
                .collect()
        })
        .collect()
}

#[test]
fn matches_zint_reference_bitmaps() {
    let enc = MicroPdf417Encoder::new();
    for &(data, cols, expected) in REFERENCE {
        // Match zint's high-level mode choice: numeric compaction for all-digit data,
        // otherwise Text compaction.
        let segment = if data.bytes().all(|b| b.is_ascii_digit()) {
            Segment::numeric(data.as_bytes().to_vec())
        } else {
            Segment::alphanumeric(data.as_bytes().to_vec())
        };
        let symbol = enc
            .build_sized(vec![segment], Some(cols))
            .unwrap_or_else(|e| panic!("build {data:?} cols {cols}: {e:?}"));
        let Encoding::Matrix(m) = enc.encode(&symbol).unwrap() else {
            panic!("expected a matrix");
        };
        let got = row_strings(&m, expected.len());
        assert_eq!(
            got.len(),
            expected.len(),
            "row count differs for {data:?} cols {cols}"
        );
        for (i, (g, e)) in got.iter().zip(expected).enumerate() {
            assert_eq!(
                g, e,
                "row {i} differs from zint for {data:?} cols {cols}\n got: {g}\n exp: {e}"
            );
        }
    }
}

/// Assert exact round-trip: decode recovers the segments and metadata, and re-encoding
/// reproduces a byte-identical matrix.
fn assert_lossless(segments: Vec<Segment>, columns: Option<usize>) {
    let enc = MicroPdf417Encoder::new();
    let symbol = enc.build_sized(segments.clone(), columns).unwrap();
    let encoding = enc.encode(&symbol).unwrap();

    let decoded = MicroPdf417Decoder::new().decode(&encoding).unwrap();
    assert_eq!(decoded.segments, symbol.segments, "segments differ");
    assert_eq!(decoded.meta, symbol.meta, "meta differs");

    let re = enc.encode(&decoded).unwrap();
    assert_eq!(encoding, re, "re-encode is not identical");
}

#[test]
fn roundtrip_text_all_columns() {
    for cols in 1..=4 {
        assert_lossless(
            vec![Segment::alphanumeric(b"Hello, MicroPDF417!".to_vec())],
            Some(cols),
        );
    }
}

#[test]
fn roundtrip_numeric_all_columns() {
    for cols in 1..=4 {
        assert_lossless(
            vec![Segment::numeric(b"9876543210123456".to_vec())],
            Some(cols),
        );
    }
}

#[test]
fn roundtrip_byte_all_columns() {
    for cols in 1..=4 {
        assert_lossless(
            vec![Segment::byte(vec![0u8, 1, 2, 3, 127, 128, 254, 255])],
            Some(cols),
        );
    }
}

#[test]
fn roundtrip_mixed_segments_autosize() {
    assert_lossless(
        vec![
            Segment::alphanumeric(b"PART".to_vec()),
            Segment::numeric(b"00427".to_vec()),
            Segment::byte(vec![0xDE, 0xAD, 0xBE, 0xEF]),
        ],
        None,
    );
}

#[test]
fn autosize_grows_with_data() {
    let enc = MicroPdf417Encoder::new();
    let small = enc.build_text("Hi").unwrap();
    let large = enc
        .build_text("The quick brown fox jumps over the lazy dog 0123456789")
        .unwrap();
    let Encoding::Matrix(sm) = enc.encode(&small).unwrap() else {
        panic!("matrix");
    };
    let Encoding::Matrix(lm) = enc.encode(&large).unwrap() else {
        panic!("matrix");
    };
    assert!(
        sm.width() * sm.height() <= lm.width() * lm.height(),
        "larger payload should not yield a smaller symbol"
    );
    // Both must still round-trip.
    assert_eq!(
        MicroPdf417Decoder::new().decode_matrix(&lm).unwrap().text(),
        large.text()
    );
}