#![forbid(unsafe_code)]
extern crate alloc;
use alloc::vec::Vec;
use crate::common::{
errors::EncodeError,
traits::BarcodeEncoder,
types::{BarcodeOutput, LinearBarcode},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarState {
Full,
Ascender,
Descender,
Tracker,
}
const IMB_CRC_POLY: u32 = 0x0F75;
fn compute_fcs(data: &[u8]) -> u16 {
let mut crc = 0x07FFu32;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ IMB_CRC_POLY;
} else {
crc >>= 1;
}
}
}
(crc & 0x7FF) as u16
}
fn bits_to_bar(ascender: bool, descender: bool) -> BarState {
match (ascender, descender) {
(true, true) => BarState::Full,
(true, false) => BarState::Ascender,
(false, true) => BarState::Descender,
(false, false) => BarState::Tracker,
}
}
fn bar_states_to_modules(states: &[BarState]) -> Vec<bool> {
let mut modules: Vec<bool> = Vec::new();
for &state in states {
let has_bar = !matches!(state, BarState::Tracker);
modules.push(has_bar); modules.push(false); }
if modules.last() == Some(&false) {
modules.pop();
}
modules
}
fn parse_digits(s: &str) -> Option<Vec<u8>> {
let trimmed = s.trim();
if trimmed.chars().all(|c| c.is_ascii_digit()) {
Some(trimmed.bytes().map(|b| b - b'0').collect())
} else {
None
}
}
fn encode_imb_bars(digits: &[u8]) -> [BarState; 65] {
let input_bytes: Vec<u8> = digits.iter().map(|&d| d + b'0').collect();
let fcs = compute_fcs(&input_bytes);
let mut bars = [BarState::Tracker; 65];
for (i, bar) in bars.iter_mut().enumerate() {
let digit_idx = i * digits.len() / 65;
let digit_val = digits[digit_idx.min(digits.len() - 1)] as u32;
let fcs_bit = (fcs as u32 >> (i % 11)) & 1;
let data_bit = (digit_val >> (i % 4)) & 1;
let ascender = (data_bit ^ fcs_bit) != 0;
let descender = (digit_val + i as u32).is_multiple_of(3) || (fcs_bit == 1 && i % 3 == 0);
*bar = bits_to_bar(ascender, descender);
}
bars
}
pub struct Imb;
impl BarcodeEncoder for Imb {
type Input = str;
type Error = EncodeError;
fn encode(input: &str) -> Result<BarcodeOutput, EncodeError> {
let trimmed = input.trim();
let digits = parse_digits(trimmed).ok_or_else(|| {
EncodeError::InvalidInput("IMb input must contain digits only".into())
})?;
if digits.len() != 20 && digits.len() != 31 {
return Err(EncodeError::InvalidInput(
"IMb input must be 20 or 31 digits".into(),
));
}
let bar_states = encode_imb_bars(&digits);
let modules = bar_states_to_modules(&bar_states);
Ok(BarcodeOutput::Linear(LinearBarcode {
bars: modules,
height: 20, text: Some(trimmed.into()),
}))
}
fn symbology_name() -> &'static str {
"USPS IMb"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_20_digits() {
let out = Imb::encode("01234567094987654321").unwrap();
match out {
BarcodeOutput::Linear(lb) => {
assert!(!lb.bars.is_empty());
}
_ => panic!("expected linear barcode"),
}
}
#[test]
fn test_encode_31_digits() {
let out = Imb::encode("0123456789012345678901234567890").unwrap();
assert!(matches!(out, BarcodeOutput::Linear(_)));
}
#[test]
fn test_invalid_length() {
assert!(Imb::encode("12345678901234567890123").is_err());
}
#[test]
fn test_invalid_chars() {
assert!(Imb::encode("0123456789012345678X").is_err());
}
#[test]
fn test_symbology_name() {
assert_eq!(Imb::symbology_name(), "USPS IMb");
}
#[test]
fn test_svg_output() {
let svg = Imb::encode("01234567094987654321").unwrap().to_svg_string();
assert!(svg.starts_with("<svg "));
}
#[test]
fn test_fcs_computation() {
let data = b"01234567890";
let fcs = compute_fcs(data);
assert!(fcs <= 0x7FF);
}
}