rxing 0.9.3

A rust port of the zxing barcode library.
Documentation
#![cfg(all(
    feature = "encoders",
    feature = "decoders",
    feature = "oned",
    feature = "qrcode",
    feature = "datamatrix"
))]

use std::collections::HashSet;

use rxing::common::HybridBinarizer;
use rxing::{
    BarcodeFormat, BinaryBitmap, DecodeHints, Luma8LuminanceSource, MultiFormatReader,
    MultiFormatWriter, Reader, Writer,
};

#[test]
fn close_applies_morphological_closing() {
    let source = Luma8LuminanceSource::new(vec![255u8; 32 * 32], 32, 32).expect("source");
    let mut bitmap = BinaryBitmap::new(HybridBinarizer::new(source));
    // a hollow 3x3 ring of black pixels; closing (dilate then erode) fills it
    for (x, y) in [
        (10, 10),
        (11, 10),
        (12, 10),
        (10, 11),
        (12, 11),
        (10, 12),
        (11, 12),
        (12, 12),
    ] {
        bitmap.get_black_matrix_mut().set(x, y);
    }
    bitmap.close().expect("close succeeds");
    let expected = "                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                X X X                                           \n                X X X                                           \n                X X X                                           \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n                                                                \n";
    assert_eq!(bitmap.get_black_matrix().to_string(), expected);
}

/// Encode `content`, render to luma8 (black = 0), optionally invert every pixel.
fn encoded_luma(format: BarcodeFormat, content: &str, inverted: bool) -> (Vec<u8>, u32, u32) {
    let bits = MultiFormatWriter
        .encode(content, &format, 300, 150)
        .expect("encode succeeds");
    let (w, h) = (bits.width(), bits.height());
    let mut luma = Vec::with_capacity((w * h) as usize);
    for y in 0..h {
        for x in 0..w {
            let v: u8 = if bits.get(x, y) { 0 } else { 255 };
            luma.push(if inverted { 255 - v } else { v });
        }
    }
    (luma, w, h)
}

fn decodes(luma: Vec<u8>, w: u32, h: u32, also_inverted: bool) -> bool {
    let hints = DecodeHints {
        AlsoInverted: Some(also_inverted),
        TryHarder: Some(true),
        ..Default::default()
    };
    let source = Luma8LuminanceSource::new(luma, w, h).expect("source");
    let mut bitmap = BinaryBitmap::new(HybridBinarizer::new(source));
    MultiFormatReader::default()
        .decode_with_hints(&mut bitmap, &hints)
        .is_ok()
}

#[test]
fn inverted_ean13_decodes_with_also_inverted_hint() {
    let (luma, w, h) = encoded_luma(BarcodeFormat::EAN_13, "5901234123457", true);
    assert!(decodes(luma, w, h, true));
}

#[test]
fn inverted_code128_decodes_with_also_inverted_hint() {
    let (luma, w, h) = encoded_luma(BarcodeFormat::CODE_128, "inversion test", true);
    assert!(decodes(luma, w, h, true));
}

#[test]
fn inverted_qr_decodes_with_also_inverted_hint() {
    let (luma, w, h) = encoded_luma(BarcodeFormat::QR_CODE, "inversion test", true);
    assert!(decodes(luma, w, h, true));
}

#[test]
fn inverted_datamatrix_decodes_with_also_inverted_hint() {
    let (luma, w, h) = encoded_luma(BarcodeFormat::DATA_MATRIX, "inversion test", true);
    assert!(decodes(luma, w, h, true));
}

#[test]
fn normal_images_decode_with_also_inverted_hint() {
    for (format, content) in [
        (BarcodeFormat::EAN_13, "5901234123457"),
        (BarcodeFormat::CODE_128, "inversion test"),
        (BarcodeFormat::QR_CODE, "inversion test"),
        (BarcodeFormat::DATA_MATRIX, "inversion test"),
    ] {
        let (luma, w, h) = encoded_luma(format, content, false);
        assert!(decodes(luma, w, h, true), "{format:?}");
    }
}

#[test]
fn inverted_image_without_hint_still_fails() {
    // inversion handling is opt-in; without the hint the decode must not succeed
    let (luma, w, h) = encoded_luma(BarcodeFormat::EAN_13, "5901234123457", true);
    assert!(!decodes(luma, w, h, false));
}

/// Transpose a WxH luma buffer, turning a horizontal barcode into a vertical
/// one (bars running top-to-bottom). Returns the (h, w) buffer.
fn transpose(luma: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    let (w, h) = (w as usize, h as usize);
    let mut out = vec![0u8; w * h];
    for y in 0..h {
        for x in 0..w {
            // new image is h wide, w tall; new(x=y, y=x)
            out[x * h + y] = luma[y * w + x];
        }
    }
    (out, h as u32, w as u32)
}

/// Decode with `PossibleFormats` restricted to a single 1D format. This forces
/// the classic `MultiFormatOneDReader` (whose TryHarder path rotates the
/// bitmap) rather than the cpp column-scanning reader.
fn decodes_1d(luma: Vec<u8>, w: u32, h: u32, format: BarcodeFormat, also_inverted: bool) -> bool {
    let hints = DecodeHints {
        PossibleFormats: Some(HashSet::from([format])),
        AlsoInverted: Some(also_inverted),
        TryHarder: Some(true),
        ..Default::default()
    };
    let source = Luma8LuminanceSource::new(luma, w, h).expect("source");
    let mut bitmap = BinaryBitmap::new(HybridBinarizer::new(source));
    MultiFormatReader::default()
        .decode_with_hints(&mut bitmap, &hints)
        .is_ok()
}

#[test]
fn normal_vertical_1d_decodes() {
    // sanity: the vertical (non-inverted) barcode decodes via the rotate path,
    // proving the test construction reaches the reader under test
    let (luma, w, h) = encoded_luma(BarcodeFormat::EAN_13, "5901234123457", false);
    let (luma, w, h) = transpose(&luma, w, h);
    assert!(decodes_1d(luma, w, h, BarcodeFormat::EAN_13, true));
}

#[test]
fn inverted_vertical_1d_decodes_with_also_inverted_hint() {
    // an inverted, vertically-oriented 1D barcode restricted to a 1D format
    // exercises the classic reader's rotate path, which must carry the
    // inverted flag into the derived (rotated) bitmap
    let (luma, w, h) = encoded_luma(BarcodeFormat::EAN_13, "5901234123457", true);
    let (luma, w, h) = transpose(&luma, w, h);
    assert!(decodes_1d(luma, w, h, BarcodeFormat::EAN_13, true));
}

#[test]
fn inverted_get_black_row_out_of_bounds_errors() {
    // While inverted, an out-of-range row must return Err (matching the
    // binarizer path's contract), not panic from indexing the flipped matrix.
    let source = Luma8LuminanceSource::new(vec![255u8; 16 * 16], 16, 16).expect("source");
    let mut bitmap = BinaryBitmap::new(HybridBinarizer::new(source));
    bitmap.invert();
    assert!(bitmap.get_black_row(9999).is_err());
    assert!(
        bitmap
            .get_black_line(9999, rxing::common::LineOrientation::Row)
            .is_err()
    );
}

#[test]
fn bitmap_state_restored_after_failed_retry() {
    // deterministic noise that fails both the normal and the inverted attempt
    let luma: Vec<u8> = (0..64 * 64)
        .map(|i| ((i as u64).wrapping_mul(2654435761) >> 8) as u8)
        .collect();
    let source = Luma8LuminanceSource::new(luma, 64, 64).expect("source");
    let mut bitmap = BinaryBitmap::new(HybridBinarizer::new(source));
    let before = bitmap.get_black_matrix().clone();

    let hints = DecodeHints {
        AlsoInverted: Some(true),
        ..Default::default()
    };
    let _ = MultiFormatReader::default().decode_with_hints(&mut bitmap, &hints);

    assert_eq!(
        *bitmap.get_black_matrix(),
        before,
        "a failed AlsoInverted retry must leave the caller's bitmap unflipped"
    );
}