#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Element {
pub bar: bool,
pub width: u32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum BarHeight {
Full,
Half,
}
pub(crate) const POSTNET_SHORT_RATIO: f32 = 0.4;
const POSTNET_PITCH_RATIO: f32 = 2.4;
const POSTNET_DIGITS: [[u8; 5]; 10] = [
[1, 1, 0, 0, 0], [0, 0, 0, 1, 1], [0, 0, 1, 0, 1], [0, 0, 1, 1, 0], [0, 1, 0, 0, 1], [0, 1, 0, 1, 0], [0, 1, 1, 0, 0], [1, 0, 0, 0, 1], [1, 0, 0, 1, 0], [1, 0, 1, 0, 0], ];
pub(crate) fn postnet_bars(data: &str) -> Vec<BarHeight> {
let digits: Vec<u8> = data
.chars()
.filter(|c| c.is_ascii_digit())
.map(|c| c as u8 - b'0')
.collect();
let sum: u32 = digits.iter().map(|&d| d as u32).sum();
let check = ((10 - (sum % 10)) % 10) as u8;
let mut bars = Vec::with_capacity(digits.len() * 5 + 7);
bars.push(BarHeight::Full); for &d in digits.iter().chain(std::iter::once(&check)) {
for &bit in &POSTNET_DIGITS[(d % 10) as usize] {
bars.push(if bit == 1 {
BarHeight::Full
} else {
BarHeight::Half
});
}
}
bars.push(BarHeight::Full); bars
}
pub(crate) fn postnet_pitch(module_width: u32) -> (u32, u32) {
let bar = module_width.max(1);
let pitch = ((bar as f32) * POSTNET_PITCH_RATIO).round() as u32;
(bar, pitch.saturating_sub(bar).max(1))
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum MsiCheck {
None,
Mod10,
Mod10Twice,
Mod11Mod10,
}
impl MsiCheck {
pub(crate) fn from_zpl(c: char) -> Self {
match c.to_ascii_uppercase() {
'A' => MsiCheck::None,
'C' => MsiCheck::Mod10Twice,
'D' => MsiCheck::Mod11Mod10,
_ => MsiCheck::Mod10,
}
}
}
fn msi_mod10(digits: &[u8]) -> u8 {
let mut doubled = String::new();
let mut rest_sum = 0u32;
for (i, &d) in digits.iter().rev().enumerate() {
if i % 2 == 0 {
doubled.insert(0, (b'0' + d) as char);
} else {
rest_sum += d as u32;
}
}
let doubled: u32 = doubled.parse::<u32>().unwrap_or(0) * 2;
let digit_sum: u32 = doubled.to_string().bytes().map(|b| (b - b'0') as u32).sum();
((10 - ((digit_sum + rest_sum) % 10)) % 10) as u8
}
fn msi_mod11(digits: &[u8]) -> u8 {
let sum: u32 = digits
.iter()
.rev()
.enumerate()
.map(|(i, &d)| d as u32 * (2 + (i % 6) as u32))
.sum();
let rem = sum % 11;
if rem == 0 { 0 } else { (11 - rem) as u8 }
}
pub(crate) fn msi_elements(
data: &str,
check: MsiCheck,
module_width: u32,
ratio: f32,
) -> Vec<Element> {
let narrow = module_width.max(1);
let wide = ((narrow as f32) * ratio).round().max(narrow as f32 + 1.0) as u32;
let mut digits: Vec<u8> = data
.chars()
.filter(|c| c.is_ascii_digit())
.map(|c| c as u8 - b'0')
.collect();
match check {
MsiCheck::None => {}
MsiCheck::Mod10 => digits.push(msi_mod10(&digits)),
MsiCheck::Mod10Twice => {
digits.push(msi_mod10(&digits));
digits.push(msi_mod10(&digits));
}
MsiCheck::Mod11Mod10 => {
digits.push(msi_mod11(&digits));
digits.push(msi_mod10(&digits));
}
}
let mut out = Vec::with_capacity(digits.len() * 8 + 5);
let push = |bar: bool, width: u32, out: &mut Vec<Element>| out.push(Element { bar, width });
push(true, wide, &mut out);
push(false, narrow, &mut out);
for &d in &digits {
for shift in (0..4).rev() {
if (d >> shift) & 1 == 1 {
push(true, wide, &mut out);
push(false, narrow, &mut out);
} else {
push(true, narrow, &mut out);
push(false, wide, &mut out);
}
}
}
push(true, narrow, &mut out);
push(false, wide, &mut out);
push(true, narrow, &mut out);
out
}
pub(crate) fn msi_text(data: &str, check: MsiCheck) -> String {
let mut digits: Vec<u8> = data
.chars()
.filter(|c| c.is_ascii_digit())
.map(|c| c as u8 - b'0')
.collect();
match check {
MsiCheck::None => {}
MsiCheck::Mod10 => digits.push(msi_mod10(&digits)),
MsiCheck::Mod10Twice => {
digits.push(msi_mod10(&digits));
digits.push(msi_mod10(&digits));
}
MsiCheck::Mod11Mod10 => {
digits.push(msi_mod11(&digits));
digits.push(msi_mod10(&digits));
}
}
digits.iter().map(|d| (b'0' + d) as char).collect()
}
pub(crate) fn is_two_width(format: rxing::BarcodeFormat) -> bool {
matches!(
format,
rxing::BarcodeFormat::CODE_39 | rxing::BarcodeFormat::ITF | rxing::BarcodeFormat::CODABAR
)
}
pub(crate) fn matrix_elements(
matrix: &rxing::common::BitMatrix,
module_width: u32,
ratio: Option<f32>,
) -> Vec<Element> {
let narrow = module_width.max(1);
let width = matrix.getWidth();
let mut out = Vec::new();
if width == 0 {
return out;
}
let mut run_start = 0u32;
let mut current = matrix.get(0, 0);
for x in 1..=width {
let value = if x < width {
matrix.get(x, 0)
} else {
!current
};
if value != current {
let modules = x - run_start;
let dots = match ratio {
Some(r) if modules > 1 => ((narrow as f32) * r).round() as u32,
Some(_) => narrow,
None => modules * narrow,
};
out.push(Element {
bar: current,
width: dots.max(1),
});
current = value;
run_start = x;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn postnet_matches_labelary_geometry() {
let bars = postnet_bars("12345");
assert_eq!(bars.len(), 32);
assert_eq!(bars[0], BarHeight::Full);
assert_eq!(bars[31], BarHeight::Full);
assert_eq!(
&bars[1..6],
&[
BarHeight::Half,
BarHeight::Half,
BarHeight::Half,
BarHeight::Full,
BarHeight::Full
]
);
let (bar_w, gap) = postnet_pitch(2);
let total = bars.len() as u32 * bar_w + (bars.len() as u32 - 1) * gap;
assert_eq!(total, 157);
}
#[test]
fn msi_check_digit_and_width() {
assert_eq!(msi_text("123456", MsiCheck::Mod10), "1234566");
let els = msi_elements("123456", MsiCheck::Mod10, 2, 2.0);
assert_eq!(els.len(), 61);
let total: u32 = els.iter().map(|e| e.width).sum();
assert_eq!(total, 182); }
#[test]
fn msi_no_check_digit_leaves_data_alone() {
assert_eq!(msi_text("123456", MsiCheck::None), "123456");
}
#[test]
fn two_width_classification() {
assert!(is_two_width(rxing::BarcodeFormat::ITF));
assert!(is_two_width(rxing::BarcodeFormat::CODE_39));
assert!(!is_two_width(rxing::BarcodeFormat::CODE_128));
assert!(!is_two_width(rxing::BarcodeFormat::EAN_13));
}
}