use crate::encoding::BitMatrix;
use crate::error::Error;
use crate::options::Options;
use crate::symbology::pdf417;
use crate::symbology::pdf417::clusters::ALL_CLUSTERS;
type Metric = (u8, u8, u8, u8, u8, u8);
#[rustfmt::skip]
const NONCCA_METRICS: [Metric; 34] = [
(1, 11, 7, 1, 0, 9), (1, 14, 7, 8, 0, 8), (1, 17, 7, 36, 0, 36),
(1, 20, 8, 19, 0, 19), (1, 24, 8, 9, 0, 17), (1, 28, 8, 25, 0, 33),
(2, 8, 8, 1, 0, 1), (2, 11, 9, 1, 0, 9), (2, 14, 9, 8, 0, 8),
(2, 17, 10, 36, 0, 36), (2, 20, 11, 19, 0, 19), (2, 23, 13, 9, 0, 17),
(2, 26, 15, 27, 0, 35), (3, 6, 12, 1, 1, 1), (3, 8, 14, 7, 7, 7),
(3, 10, 16, 15, 15, 15), (3, 12, 18, 25, 25, 25), (3, 15, 21, 37, 37, 37),
(3, 20, 26, 1, 17, 33), (3, 26, 32, 1, 9, 17), (3, 32, 38, 21, 29, 37),
(3, 38, 44, 15, 31, 47), (3, 44, 50, 1, 25, 49), (4, 4, 8, 47, 19, 43),
(4, 6, 12, 1, 1, 1), (4, 8, 14, 7, 7, 7), (4, 10, 16, 15, 15, 15),
(4, 12, 18, 25, 25, 25), (4, 15, 21, 37, 37, 37), (4, 20, 26, 1, 17, 33),
(4, 26, 32, 1, 9, 17), (4, 32, 38, 21, 29, 37), (4, 38, 44, 15, 31, 47),
(4, 44, 50, 1, 25, 49),
];
#[rustfmt::skip]
#[allow(dead_code)]
const CCA_METRICS: [Metric; 17] = [
(2, 5, 4, 39, 0, 19), (2, 6, 4, 1, 0, 33), (2, 7, 5, 32, 0, 12),
(2, 8, 5, 8, 0, 40), (2, 9, 6, 14, 0, 46), (2, 10, 6, 43, 0, 23),
(2, 12, 7, 20, 0, 52), (3, 4, 4, 11, 43, 23), (3, 5, 5, 1, 33, 13),
(3, 6, 6, 5, 37, 17), (3, 7, 7, 15, 47, 27), (3, 8, 7, 21, 1, 33),
(4, 3, 4, 40, 20, 52), (4, 4, 5, 43, 23, 3), (4, 5, 6, 46, 26, 6),
(4, 6, 7, 34, 14, 46), (4, 7, 8, 29, 9, 41),
];
#[rustfmt::skip]
pub(crate) const RAPS: [[u16; 52]; 2] = [
[
802, 930, 946, 818, 882, 890, 826, 954, 922, 986, 970, 906, 778, 794, 786,
914, 978, 982, 980, 916, 948, 932, 934, 942, 940, 936, 808, 812, 814, 806,
822, 950, 918, 790, 788, 820, 884, 868, 870, 878, 876, 872, 840, 856, 860,
862, 846, 844, 836, 838, 834, 866,
],
[
718, 590, 622, 558, 550, 566, 534, 530, 538, 570, 562, 546, 610, 626, 634,
762, 754, 758, 630, 628, 612, 614, 582, 578, 706, 738, 742, 740, 748, 620,
556, 552, 616, 744, 712, 716, 708, 710, 646, 654, 652, 668, 664, 696, 688,
656, 720, 592, 600, 604, 732, 734,
],
];
const LATCHES: [u16; 5] = [900, 901, 902, 913, 924];
pub(crate) fn data_codewords(input: &[u8]) -> Result<Vec<u16>, String> {
let pdf_cws = pdf417::data_codewords(input)?;
if pdf_cws.first().is_some_and(|cw| LATCHES.contains(cw)) {
Ok(pdf_cws)
} else {
let mut out = Vec::with_capacity(pdf_cws.len() + 1);
out.push(900);
out.extend_from_slice(&pdf_cws);
Ok(out)
}
}
pub(crate) fn select_metric(datcws_len: usize) -> Option<Metric> {
NONCCA_METRICS
.iter()
.copied()
.find(|&(c, r, k, _, _, _)| (c as usize) * (r as usize) - (k as usize) >= datcws_len)
}
pub(crate) fn select_metric_constrained(datcws_len: usize, ucols: u8, urows: u8) -> Option<Metric> {
NONCCA_METRICS.iter().copied().find(|&(c, r, k, _, _, _)| {
let capacity = (c as usize) * (r as usize) - (k as usize);
capacity >= datcws_len && (ucols == 0 || ucols == c) && (urows == 0 || urows == r)
})
}
pub(crate) fn select_cca_metric(cw_count: usize, ucols: u8) -> Option<Metric> {
select_cca_metric_constrained(cw_count, ucols, 0)
}
pub(crate) fn select_cca_metric_constrained(
cw_count: usize,
ucols: u8,
urows: u8,
) -> Option<Metric> {
CCA_METRICS.iter().copied().find(|&(c, r, k, _, _, _)| {
let ncws = (c as usize) * (r as usize) - (k as usize);
ncws >= cw_count && (ucols == 0 || ucols == c) && (urows == 0 || urows == r)
})
}
const CCA_ROW_WIDTHS: [usize; 3] = [55, 72, 99];
pub(crate) fn select_ccb_metric(datcws_len: usize, ucols: u8) -> Option<Metric> {
NONCCA_METRICS.iter().copied().find(|&(c, r, k, _, _, _)| {
let ncws = (c as usize) * (r as usize) - (k as usize);
ncws >= datcws_len && (ucols == 0 || ucols == c)
})
}
pub(crate) fn pack_ccb_datcws(bytes: &[u8]) -> Vec<u16> {
let n_groups = bytes.len() / 6;
let rem = bytes.len() % 6;
let mut out: Vec<u16> = Vec::with_capacity(n_groups * 5 + rem + 2);
out.push(920);
out.push(if rem == 0 { 924 } else { 901 });
for g in 0..n_groups {
let v: u64 = u64::from(bytes[g * 6]) << 40
| u64::from(bytes[g * 6 + 1]) << 32
| u64::from(bytes[g * 6 + 2]) << 24
| u64::from(bytes[g * 6 + 3]) << 16
| u64::from(bytes[g * 6 + 4]) << 8
| u64::from(bytes[g * 6 + 5]);
let mut digits = [0u16; 5];
let mut x = v;
for d in &mut digits {
*d = (x % 900) as u16;
x /= 900;
}
debug_assert_eq!(x, 0, "6-byte → 5-digit base-900 conversion overflowed");
for &d in digits.iter().rev() {
out.push(d);
}
}
for b in &bytes[n_groups * 6..] {
out.push(u16::from(*b));
}
debug_assert_eq!(out.len(), n_groups * 5 + rem + 2);
out
}
pub(crate) fn ccb_cws_compose(bytes: &[u8], ucols: u8) -> Result<(Vec<u16>, Metric), String> {
let datcws = pack_ccb_datcws(bytes);
let metric = select_ccb_metric(datcws.len(), ucols).ok_or_else(|| {
format!(
"CC-B: {} datcws with cols={ucols} doesn't fit any size",
datcws.len(),
)
})?;
let (c, r, k, _, _, _) = metric;
let c = c as usize;
let r = r as usize;
let k = k as usize;
let n = c * r - k;
let mut cws: Vec<u16> = Vec::with_capacity(c * r);
cws.extend_from_slice(&datcws);
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
Ok((cws, metric))
}
pub(crate) fn render_ccb(bytes: &[u8], ucols: u8) -> Result<(BitMatrix, Metric), String> {
let (cws, metric) = ccb_cws_compose(bytes, ucols)?;
let bm = render_cws(&cws, metric)?;
Ok((bm, metric))
}
pub(crate) fn cws_compose(input: &[u8]) -> Result<(Vec<u16>, Metric), String> {
let datcws = data_codewords(input)?;
let metric = select_metric(datcws.len())
.ok_or_else(|| format!("MicroPDF417: data too long ({} codewords)", datcws.len()))?;
let (c, r, k, _, _, _) = metric;
let c = c as usize;
let r = r as usize;
let k = k as usize;
let n = c * r - k;
let mut cws: Vec<u16> = Vec::with_capacity(c * r);
cws.extend_from_slice(&datcws);
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
Ok((cws, metric))
}
const ROW_WIDTHS: [usize; 4] = [38, 55, 82, 99];
fn append_bits(out: &mut Vec<u8>, value: u32, n: usize) {
for i in 0..n {
out.push(((value >> (n - 1 - i)) & 1) as u8);
}
}
pub(crate) fn render(input: &[u8]) -> Result<BitMatrix, String> {
let (cws, metric) = cws_compose(input)?;
render_cws(&cws, metric)
}
pub(crate) fn render_cws(cws: &[u16], metric: Metric) -> Result<BitMatrix, String> {
let (c, r, _k, rapl, rapc, rapr) = metric;
let c = c as usize;
let r = r as usize;
let rapl = rapl as usize;
let rapc = rapc as usize;
let rapr = rapr as usize;
let rwid = ROW_WIDTHS[c - 1];
let mut bm = BitMatrix::new(rwid, r);
for i in 0..r {
let clst = (i + rapl - 1) % 3;
let l_pos = (i + rapl - 1) % 52;
let r_pos = (i + rapr - 1) % 52;
let l_rap = RAPS[0][l_pos] as u32;
let r_rap = RAPS[0][r_pos] as u32;
let centre_rap = if rapc > 0 {
let c_pos = (i + rapc - 1) % 52;
RAPS[1][c_pos] as u32
} else {
0
};
let mut row: Vec<u8> = Vec::with_capacity(rwid);
match c {
1 => {
append_bits(&mut row, l_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i] as usize], 17);
append_bits(&mut row, r_rap, 10);
}
2 => {
append_bits(&mut row, l_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 2] as usize], 17);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 2 + 1] as usize], 17);
append_bits(&mut row, r_rap, 10);
}
3 => {
append_bits(&mut row, l_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 3] as usize], 17);
append_bits(&mut row, centre_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 3 + 1] as usize], 17);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 3 + 2] as usize], 17);
append_bits(&mut row, r_rap, 10);
}
4 => {
append_bits(&mut row, l_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 4] as usize], 17);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 4 + 1] as usize], 17);
append_bits(&mut row, centre_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 4 + 2] as usize], 17);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 4 + 3] as usize], 17);
append_bits(&mut row, r_rap, 10);
}
_ => unreachable!("metrics table only contains c ∈ {{1, 2, 3, 4}}"),
}
row.push(1); debug_assert_eq!(row.len(), rwid);
for (x, &bit) in row.iter().enumerate() {
bm.set(x, i, bit != 0);
}
}
Ok(bm)
}
#[allow(dead_code)]
pub(crate) fn render_cca(cws_in: &[u32], ucols: u8) -> Result<(BitMatrix, Metric), String> {
let metric = select_cca_metric(cws_in.len(), ucols).ok_or_else(|| {
format!(
"CC-A: {} codewords with cols={ucols} doesn't fit any size",
cws_in.len(),
)
})?;
let (c, r, k, _, _, _) = metric;
let c = c as usize;
let r = r as usize;
let k = k as usize;
let n = c * r - k;
let mut cws: Vec<u16> = cws_in.iter().map(|&v| v as u16).collect();
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
let bm = render_cca_cws(&cws, metric)?;
Ok((bm, metric))
}
pub(crate) fn render_cca_cws(cws: &[u16], metric: Metric) -> Result<BitMatrix, String> {
let (c, r, _k, rapl, rapc, rapr) = metric;
let c = c as usize;
let r = r as usize;
let rapl = rapl as usize;
let rapc = rapc as usize;
let rapr = rapr as usize;
let rwid = CCA_ROW_WIDTHS[c - 2];
let mut bm = BitMatrix::new(rwid, r);
for i in 0..r {
let clst = (i + rapl - 1) % 3;
let l_pos = (i + rapl - 1) % 52;
let r_pos = (i + rapr - 1) % 52;
let l_rap = RAPS[0][l_pos] as u32;
let r_rap = RAPS[0][r_pos] as u32;
let centre_rap = if rapc > 0 {
let c_pos = (i + rapc - 1) % 52;
RAPS[1][c_pos] as u32
} else {
0
};
let mut row: Vec<u8> = Vec::with_capacity(rwid);
match c {
2 => {
append_bits(&mut row, l_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 2] as usize], 17);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 2 + 1] as usize], 17);
append_bits(&mut row, r_rap, 10);
}
3 => {
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 3] as usize], 17);
append_bits(&mut row, centre_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 3 + 1] as usize], 17);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 3 + 2] as usize], 17);
append_bits(&mut row, r_rap, 10);
}
4 => {
append_bits(&mut row, l_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 4] as usize], 17);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 4 + 1] as usize], 17);
append_bits(&mut row, centre_rap, 10);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 4 + 2] as usize], 17);
append_bits(&mut row, ALL_CLUSTERS[clst][cws[i * 4 + 3] as usize], 17);
append_bits(&mut row, r_rap, 10);
}
_ => unreachable!("CC-A metrics table contains only c ∈ {{2, 3, 4}}"),
}
row.push(1); debug_assert_eq!(row.len(), rwid);
for (x, &bit) in row.iter().enumerate() {
bm.set(x, i, bit != 0);
}
}
Ok(bm)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MicroPdf417Mode {
Auto,
Ccb,
Cca,
Raw,
Parse,
Parsefnc,
}
const PARSEINPUT_CTRL_NAMES: &[(&str, u8)] = &[
("NUL", 0),
("SOH", 1),
("STX", 2),
("ETX", 3),
("EOT", 4),
("ENQ", 5),
("ACK", 6),
("BEL", 7),
("BS", 8),
("TAB", 9),
("LF", 10),
("VT", 11),
("FF", 12),
("CR", 13),
("DLE", 16),
("DC1", 17),
("DC2", 18),
("DC3", 19),
("DC4", 20),
("NAK", 21),
("SYN", 22),
("ETB", 23),
("CAN", 24),
("EM", 25),
("SUB", 26),
("ESC", 27),
("FS", 28),
("GS", 29),
("RS", 30),
("US", 31),
];
fn parse_text_escapes(input: &[u8]) -> Result<Vec<u8>, String> {
let mut out: Vec<u8> = Vec::with_capacity(input.len());
let mut i = 0;
while i < input.len() {
let c = input[i];
if c != b'^' {
out.push(c);
i += 1;
continue;
}
let mut matched = false;
if i + 1 + 3 <= input.len() {
let candidate = &input[i + 1..i + 4];
if let Some((_, byte)) = PARSEINPUT_CTRL_NAMES
.iter()
.find(|(name, _)| name.len() == 3 && name.as_bytes() == candidate)
{
out.push(*byte);
i += 4;
matched = true;
}
}
if !matched && i + 1 + 2 <= input.len() {
let candidate = &input[i + 1..i + 3];
if let Some((_, byte)) = PARSEINPUT_CTRL_NAMES
.iter()
.find(|(name, _)| name.len() == 2 && name.as_bytes() == candidate)
{
out.push(*byte);
i += 3;
matched = true;
}
}
if !matched && i + 1 + 3 <= input.len() {
let candidate = &input[i + 1..i + 4];
if candidate.iter().all(|b| b.is_ascii_digit()) {
let value: u32 = (u32::from(candidate[0] - b'0') * 100)
+ (u32::from(candidate[1] - b'0') * 10)
+ u32::from(candidate[2] - b'0');
if value > 255 {
return Err(format!(
"micropdf417 parse: ordinal must be 000..=255; got {value}",
));
}
out.push(value as u8);
i += 4;
matched = true;
}
}
if !matched {
out.push(b'^');
i += 1;
}
}
Ok(out)
}
fn parse_text_parsefnc(input: &[u8]) -> Result<(Vec<u16>, Vec<u8>), String> {
let mut eci_codewords: Vec<u16> = Vec::new();
let mut i = 0;
while i + 1 + 9 <= input.len() && &input[i..i + 4] == b"^ECI" {
let digits = &input[i + 4..i + 10];
if !digits.iter().all(|b| b.is_ascii_digit()) {
return Err(format!(
"micropdf417 parsefnc: ^ECI must be followed by 6 ASCII digits; \
got {:?}",
std::str::from_utf8(digits).unwrap_or("<non-utf8>"),
));
}
let mut value: u32 = 0;
for &d in digits {
value = value * 10 + u32::from(d - b'0');
}
if value > 999_999 {
return Err(format!(
"micropdf417 parsefnc: ECI value must be 000000..=999999; got {value}",
));
}
eci_codewords.push(927);
eci_codewords.push(value as u16);
i += 10;
}
let mut out: Vec<u8> = Vec::with_capacity(input.len() - i);
let mut j = i;
while j < input.len() {
let c = input[j];
if c == b'^' {
if j + 1 < input.len() && input[j + 1] == b'^' {
out.push(b'^');
j += 2;
continue;
}
if j + 4 <= input.len() && &input[j..j + 4] == b"^ECI" {
return Err(
"micropdf417 parsefnc: mid-stream ^ECI markers are not supported; \
place ECI prefixes only at the start of the input"
.into(),
);
}
out.push(b'^');
j += 1;
continue;
}
out.push(c);
j += 1;
}
Ok((eci_codewords, out))
}
fn render_with_eci_prefix(eci_codewords: &[u16], text_bytes: &[u8]) -> Result<BitMatrix, String> {
let text_datcws = data_codewords(text_bytes)?;
let mut datcws: Vec<u16> = Vec::with_capacity(eci_codewords.len() + text_datcws.len());
datcws.extend_from_slice(eci_codewords);
datcws.extend_from_slice(&text_datcws);
let metric = select_metric(datcws.len()).ok_or_else(|| {
format!(
"MicroPDF417 parsefnc: {} codewords doesn't fit any non-CCA metric",
datcws.len(),
)
})?;
let (c, r, k, _, _, _) = metric;
let c = c as usize;
let r = r as usize;
let k = k as usize;
let n = c * r - k;
let mut cws: Vec<u16> = Vec::with_capacity(c * r);
cws.extend_from_slice(&datcws);
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
render_cws(&cws, metric)
}
fn parse_raw_codewords(input: &[u8]) -> Result<Vec<u32>, String> {
let mut out: Vec<u32> = Vec::with_capacity(input.len() / 4);
let mut i = 0;
while i + 4 <= input.len() {
if input[i] != b'^' {
return Err(format!(
"micropdf417: raw codewords must be formatted as ^NNN; \
expected `^` at offset {i}, got 0x{:02X}",
input[i],
));
}
let mut value: u32 = 0;
for j in 1..=3 {
let c = input[i + j];
if !c.is_ascii_digit() {
return Err(format!(
"micropdf417: raw codewords must be formatted as ^NNN; \
non-digit 0x{c:02X} at offset {}",
i + j,
));
}
value = value * 10 + u32::from(c - b'0');
}
if value > 928 {
return Err(format!(
"micropdf417: raw codewords must be 0..=928; got {value}",
));
}
out.push(value);
i += 4;
}
if i != input.len() {
return Err(format!(
"micropdf417: raw codewords must be formatted as ^NNN; \
{} trailing byte(s) at offset {i}",
input.len() - i,
));
}
Ok(out)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct SizeConstraints {
pub ucols: u8,
pub urows: u8,
}
impl SizeConstraints {
pub(crate) fn is_default(&self) -> bool {
self.ucols == 0 && self.urows == 0
}
}
fn parse_version_string(v: &str) -> Result<(u8, u8), Error> {
let Some((rows_str, cols_str)) = v.split_once('x') else {
return Err(Error::InvalidOption(format!(
"micropdf417: version={v:?} must be formatted as RxC"
)));
};
if rows_str.is_empty() || cols_str.is_empty() {
return Err(Error::InvalidOption(format!(
"micropdf417: version={v:?} must be formatted as RxC"
)));
}
if !rows_str.chars().all(|c| c.is_ascii_digit())
|| !cols_str.chars().all(|c| c.is_ascii_digit())
{
return Err(Error::InvalidOption(format!(
"micropdf417: version={v:?} must be formatted as RxC"
)));
}
let rows: u8 = rows_str.parse().map_err(|_| {
Error::InvalidOption(format!(
"micropdf417: version={v:?} rows component out of range"
))
})?;
let cols: u8 = cols_str.parse().map_err(|_| {
Error::InvalidOption(format!(
"micropdf417: version={v:?} columns component out of range"
))
})?;
Ok((rows, cols))
}
fn check_micropdf417_opts(opts: &Options) -> Result<(MicroPdf417Mode, SizeConstraints), Error> {
let mut size = SizeConstraints::default();
if let Some(v) = opts.get("version") {
if v != "unset" {
let (rows, cols) = parse_version_string(v)?;
size.urows = rows;
size.ucols = cols;
}
}
let mut mode = MicroPdf417Mode::Auto;
if let Some(v) = opts.get("ccb") {
match v {
"false" => {}
"true" => mode = MicroPdf417Mode::Ccb,
_ => {
return Err(Error::InvalidOption(format!(
"micropdf417: ccb={v:?} must be \"true\" or \"false\""
)));
}
}
}
if let Some(v) = opts.get("cca") {
match v {
"false" => {}
"true" => {
if mode == MicroPdf417Mode::Ccb {
return Err(Error::InvalidOption(
"micropdf417: cca=true and ccb=true cannot be combined".into(),
));
}
mode = MicroPdf417Mode::Cca;
}
_ => {
return Err(Error::InvalidOption(format!(
"micropdf417: cca={v:?} must be \"true\" or \"false\""
)));
}
}
}
if let Some(v) = opts.get("raw") {
match v {
"false" => {}
"true" => {
if mode != MicroPdf417Mode::Auto {
return Err(Error::InvalidOption(
"micropdf417: raw=true cannot be combined with cca=true or ccb=true".into(),
));
}
mode = MicroPdf417Mode::Raw;
}
_ => {
return Err(Error::InvalidOption(format!(
"micropdf417: raw={v:?} must be \"true\" or \"false\""
)));
}
}
}
if let Some(v) = opts.get("parse") {
match v {
"false" => {}
"true" => {
if mode == MicroPdf417Mode::Auto {
mode = MicroPdf417Mode::Parse;
}
}
_ => {
return Err(Error::InvalidOption(format!(
"micropdf417: parse={v:?} must be \"true\" or \"false\""
)));
}
}
}
if let Some(v) = opts.get("parsefnc") {
match v {
"false" => {}
"true" => {
if mode == MicroPdf417Mode::Auto {
mode = MicroPdf417Mode::Parsefnc;
} else if mode == MicroPdf417Mode::Parse {
mode = MicroPdf417Mode::Parsefnc;
}
}
_ => {
return Err(Error::InvalidOption(format!(
"micropdf417: parsefnc={v:?} must be \"true\" or \"false\""
)));
}
}
}
for key in ["columns", "rows"] {
if let Some(v) = opts.get(key) {
let parsed: u8 = v.parse().map_err(|_| {
Error::InvalidOption(format!(
"micropdf417: {key}={v:?} must be a non-negative integer"
))
})?;
if parsed != 0 {
if key == "columns" {
size.ucols = parsed;
} else {
size.urows = parsed;
}
}
}
}
Ok((mode, size))
}
pub fn encode(data: &str, opts: &Options) -> Result<BitMatrix, Error> {
let (mode, size) = check_micropdf417_opts(opts)?;
match mode {
MicroPdf417Mode::Auto => {
render_constrained(data.as_bytes(), size).map_err(Error::InvalidData)
}
MicroPdf417Mode::Ccb => render_ccb_constrained(data.as_bytes(), size)
.map(|(bm, _)| bm)
.map_err(Error::InvalidData),
MicroPdf417Mode::Cca => {
let codewords = parse_raw_codewords(data.as_bytes()).map_err(Error::InvalidData)?;
render_cca_constrained(&codewords, size)
.map(|(bm, _)| bm)
.map_err(Error::InvalidData)
}
MicroPdf417Mode::Raw => {
let codewords = parse_raw_codewords(data.as_bytes()).map_err(Error::InvalidData)?;
render_raw_codewords_constrained(&codewords, size).map_err(Error::InvalidData)
}
MicroPdf417Mode::Parse => {
let substituted = parse_text_escapes(data.as_bytes()).map_err(Error::InvalidData)?;
render_constrained(&substituted, size).map_err(Error::InvalidData)
}
MicroPdf417Mode::Parsefnc => {
let (eci_codewords, substituted) =
parse_text_parsefnc(data.as_bytes()).map_err(Error::InvalidData)?;
if eci_codewords.is_empty() {
render_constrained(&substituted, size).map_err(Error::InvalidData)
} else {
render_with_eci_prefix_constrained(&eci_codewords, &substituted, size)
.map_err(Error::InvalidData)
}
}
}
}
fn render_constrained(input: &[u8], size: SizeConstraints) -> Result<BitMatrix, String> {
if size.is_default() {
return render(input);
}
let datcws = data_codewords(input)?;
let metric =
select_metric_constrained(datcws.len(), size.ucols, size.urows).ok_or_else(|| {
format!(
"MicroPDF417: no metric for {} datcws with constraints \
columns={} rows={}",
datcws.len(),
size.ucols,
size.urows,
)
})?;
let (c, r, k, _, _, _) = metric;
let (c, r, k) = (c as usize, r as usize, k as usize);
let n = c * r - k;
let mut cws: Vec<u16> = Vec::with_capacity(c * r);
cws.extend_from_slice(&datcws);
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
render_cws(&cws, metric)
}
fn render_ccb_constrained(
bytes: &[u8],
size: SizeConstraints,
) -> Result<(BitMatrix, Metric), String> {
if size.is_default() {
return render_ccb(bytes, 0);
}
let datcws = pack_ccb_datcws(bytes);
let metric =
select_metric_constrained(datcws.len(), size.ucols, size.urows).ok_or_else(|| {
format!(
"CC-B: no metric for {} datcws with constraints \
columns={} rows={}",
datcws.len(),
size.ucols,
size.urows,
)
})?;
let (c, r, k, _, _, _) = metric;
let (c, r, k) = (c as usize, r as usize, k as usize);
let n = c * r - k;
let mut cws: Vec<u16> = Vec::with_capacity(c * r);
cws.extend_from_slice(&datcws);
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
let bm = render_cws(&cws, metric)?;
Ok((bm, metric))
}
fn render_cca_constrained(
cws_in: &[u32],
size: SizeConstraints,
) -> Result<(BitMatrix, Metric), String> {
if size.is_default() {
return render_cca(cws_in, 0);
}
let metric =
select_cca_metric_constrained(cws_in.len(), size.ucols, size.urows).ok_or_else(|| {
format!(
"CC-A: no metric for {} codewords with constraints \
columns={} rows={}",
cws_in.len(),
size.ucols,
size.urows,
)
})?;
let (c, r, k, _, _, _) = metric;
let (c, r, k) = (c as usize, r as usize, k as usize);
let n = c * r - k;
let mut cws: Vec<u16> = cws_in.iter().map(|&v| v as u16).collect();
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
let bm = render_cca_cws(&cws, metric)?;
Ok((bm, metric))
}
fn render_raw_codewords_constrained(
cws_in: &[u32],
size: SizeConstraints,
) -> Result<BitMatrix, String> {
if size.is_default() {
return render_raw_codewords(cws_in);
}
let metric =
select_metric_constrained(cws_in.len(), size.ucols, size.urows).ok_or_else(|| {
format!(
"micropdf417 raw: no metric for {} codewords with constraints \
columns={} rows={}",
cws_in.len(),
size.ucols,
size.urows,
)
})?;
let (c, r, k, _, _, _) = metric;
let (c, r, k) = (c as usize, r as usize, k as usize);
let n = c * r - k;
let mut cws: Vec<u16> = cws_in.iter().map(|&v| v as u16).collect();
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
render_cws(&cws, metric)
}
fn render_with_eci_prefix_constrained(
eci_codewords: &[u16],
text_bytes: &[u8],
size: SizeConstraints,
) -> Result<BitMatrix, String> {
if size.is_default() {
return render_with_eci_prefix(eci_codewords, text_bytes);
}
let text_datcws = data_codewords(text_bytes)?;
let mut datcws: Vec<u16> = Vec::with_capacity(eci_codewords.len() + text_datcws.len());
datcws.extend_from_slice(eci_codewords);
datcws.extend_from_slice(&text_datcws);
let metric =
select_metric_constrained(datcws.len(), size.ucols, size.urows).ok_or_else(|| {
format!(
"MicroPDF417 parsefnc: no metric for {} datcws with constraints \
columns={} rows={}",
datcws.len(),
size.ucols,
size.urows,
)
})?;
let (c, r, k, _, _, _) = metric;
let (c, r, k) = (c as usize, r as usize, k as usize);
let n = c * r - k;
let mut cws: Vec<u16> = Vec::with_capacity(c * r);
cws.extend_from_slice(&datcws);
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
render_cws(&cws, metric)
}
fn render_raw_codewords(cws_in: &[u32]) -> Result<BitMatrix, String> {
let metric = select_metric(cws_in.len()).ok_or_else(|| {
format!(
"micropdf417 raw: {} codewords doesn't fit any non-CCA metric",
cws_in.len(),
)
})?;
let (c, r, k, _, _, _) = metric;
let c = c as usize;
let r = r as usize;
let k = k as usize;
let n = c * r - k;
let mut cws: Vec<u16> = cws_in.iter().map(|&v| v as u16).collect();
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws.extend_from_slice(&check);
debug_assert_eq!(cws.len(), c * r);
render_cws(&cws, metric)
}
#[cfg(test)]
mod tests {
use super::*;
fn goldens() -> &'static [(&'static str, &'static [u16], &'static [u16], Metric)] {
&[
(
"PDF417",
&[900, 453, 178, 121, 239],
&[
900, 453, 178, 121, 239, 900, 900, 393, 904, 95, 716, 3, 811, 744,
],
(1, 14, 7, 8, 0, 8),
),
(
"Hello, World!",
&[900, 237, 131, 344, 883, 807, 674, 521, 119, 329],
&[
900, 237, 131, 344, 883, 807, 674, 521, 119, 329, 899, 128, 32, 538, 603, 7,
568,
],
(1, 17, 7, 36, 0, 36),
),
(
"1234567890",
&[902, 15, 369, 753, 190],
&[
902, 15, 369, 753, 190, 900, 900, 467, 14, 648, 330, 592, 604, 219,
],
(1, 14, 7, 8, 0, 8),
),
]
}
#[test]
fn select_cca_metric_picks_smallest_fitting() {
let m = select_cca_metric(8, 4)
.expect("select_cca_metric(8 cws, ucols=4) must return Some((c=4, r=3, k=4, ..)) — smallest 4-col fit");
assert_eq!(m, (4, 3, 4, 40, 20, 52));
let m = select_cca_metric(8, 2)
.expect("select_cca_metric(8 cws, ucols=2) must return Some((c=2, r=6, k=4, ..)) — smallest 2-col fit (2nd row of CCA_METRICS)");
assert_eq!(m, (2, 6, 4, 1, 0, 33));
assert!(
select_cca_metric(100, 4).is_none(),
"select_cca_metric(100 cws, ucols=4) must return None — 100 exceeds the largest CC-A metric capacity at any column count"
);
}
#[test]
fn render_cca_matches_bwip_js_gtin() {
let enc = crate::symbology::gs1_cc::encode_cc("(01)12345678901231", 4).expect(
"gs1_cc::encode_cc(\"(01)12345678901231\", 4 cols) (canonical GTIN-14 via CC-A method 0) must succeed",
);
let (bm, metric) = super::render_cca(&enc.codewords, 4).expect(
"render_cca(cws, ucols=4) (CC-A render at 4 columns, expected (4,3,4,..) metric → rwid=99 rows=3, 297-cell pixs) must succeed",
);
assert_eq!(metric, (4, 3, 4, 40, 20, 52));
let want: [u8; 297] = [
1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1,
0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0,
0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0,
0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1,
0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0,
0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1,
0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1,
1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0,
1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
1, 0, 0, 0, 1, 0, 1,
];
for y in 0..3 {
for x in 0..99 {
let got = u8::from(bm.get(x, y));
let w = want[y * 99 + x];
assert_eq!(got, w, "mismatch at ({x}, {y})");
}
}
}
#[test]
fn render_cca_smoke_test() {
let enc = crate::symbology::gs1_cc::encode_cc("(01)12345678901231", 4).expect(
"gs1_cc::encode_cc(\"(01)12345678901231\", 4 cols) (dimension-only smoke-test setup) must succeed",
);
let cws: Vec<u32> = enc.codewords;
let (bm, metric) = super::render_cca(&cws, 4).expect(
"render_cca(8 cws, ucols=4) (dimension-only smoke-test, expecting CCA_ROW_WIDTHS[2]=99 wide × 3 tall) must succeed",
);
assert_eq!(metric, (4, 3, 4, 40, 20, 52));
assert_eq!(bm.width(), super::CCA_ROW_WIDTHS[2]); assert_eq!(bm.height(), 3);
}
#[test]
fn data_codewords_match_bwip_js() {
for &(text, want, _, _) in goldens() {
let got = data_codewords(text.as_bytes()).unwrap();
assert_eq!(got, want, "datcws mismatch for {text:?}");
}
}
#[test]
fn cws_compose_matches_bwip_js() {
for &(text, _, want_cws, want_metric) in goldens() {
let (got_cws, got_metric) = cws_compose(text.as_bytes()).unwrap();
assert_eq!(got_cws, want_cws, "cws mismatch for {text:?}");
assert_eq!(got_metric, want_metric, "metric mismatch for {text:?}");
}
}
#[test]
fn select_metric_picks_smallest_fit() {
assert_eq!(select_metric(5), Some((1, 14, 7, 8, 0, 8)));
assert_eq!(select_metric(10), Some((1, 17, 7, 36, 0, 36)));
assert_eq!(select_metric(177), None);
}
#[test]
fn render_matches_bwip_js_pixs_c1() {
#[rustfmt::skip]
let want_pixs: [u8; 532] = [
1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1,
1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1,
1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1,
0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1,
0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1,
0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1,
0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1,
0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1,
1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0,
1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1,
0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1,
0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0,
0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1,
0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0,
1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1,
0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1,
1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0,
1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0,
1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1,
0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1,
0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1,
0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0,
1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0,
0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1,
1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1,
0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0,
1, 0, 0, 1,
];
let bm = render(b"PDF417").unwrap();
assert_eq!(bm.width(), 38);
assert_eq!(bm.height(), 14);
let mut got: Vec<u8> = Vec::with_capacity(bm.width() * bm.height());
for y in 0..bm.height() {
for x in 0..bm.width() {
got.push(bm.get(x, y) as u8);
}
}
assert_eq!(got, want_pixs);
}
#[test]
fn render_matches_bwip_js_pixs_c2() {
let cws_c2: [u16; 22] = [
900, 237, 131, 344, 883, 807, 674, 521, 119, 329, 900, 900, 900, 640, 562, 805, 162,
290, 508, 845, 886, 7,
];
let metric_c2: Metric = (2, 11, 9, 1, 0, 9);
#[rustfmt::skip]
let want_pixs: [u8; 605] = [
1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0,
1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0,
0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0,
0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1,
1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1,
1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1,
1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1,
1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0,
1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1,
1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0,
1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0,
0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1,
1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1,
1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1,
1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0,
1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1,
1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1,
1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1,
0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0,
0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0,
1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1,
0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1,
0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1,
0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0,
1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0,
0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1,
1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1,
1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1,
0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0,
1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0,
1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1,
0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1,
1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1,
];
let bm = render_cws(&cws_c2, metric_c2).unwrap();
assert_eq!(bm.width(), 55);
assert_eq!(bm.height(), 11);
let mut got: Vec<u8> = Vec::with_capacity(bm.width() * bm.height());
for y in 0..bm.height() {
for x in 0..bm.width() {
got.push(bm.get(x, y) as u8);
}
}
assert_eq!(got, want_pixs);
}
#[test]
fn render_matches_bwip_js_pixs_c3() {
let cws_c3: [u16; 24] = [
900, 237, 131, 344, 883, 807, 674, 521, 119, 329, 844, 915, 199, 454, 360, 748, 341,
841, 760, 135, 444, 2, 762, 884,
];
let metric_c3: Metric = (3, 8, 14, 7, 7, 7);
#[rustfmt::skip]
let want_pixs: [u8; 656] = [
1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0,
1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0,
0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1,
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1,
0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1,
0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0,
0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0,
1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1,
0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0,
0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1,
0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1,
1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0,
1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0,
1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0,
1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1,
0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1,
1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0,
1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1,
1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1,
1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1,
1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0,
1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1,
0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0,
0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1,
0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1,
0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0,
1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1,
0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1,
0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0,
0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1,
0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1,
];
let bm = render_cws(&cws_c3, metric_c3).unwrap();
assert_eq!(bm.width(), 82);
assert_eq!(bm.height(), 8);
let mut got: Vec<u8> = Vec::with_capacity(bm.width() * bm.height());
for y in 0..bm.height() {
for x in 0..bm.width() {
got.push(bm.get(x, y) as u8);
}
}
assert_eq!(got, want_pixs);
}
#[test]
fn render_matches_bwip_js_pixs_c4() {
let cws_c4: [u16; 24] = [
900, 237, 131, 344, 883, 807, 674, 521, 119, 329, 900, 900, 827, 207, 535, 470, 340,
64, 712, 198, 866, 136, 221, 283,
];
let metric_c4: Metric = (4, 6, 12, 1, 1, 1);
#[rustfmt::skip]
let want_pixs: [u8; 594] = [
1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0,
1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1,
0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1,
0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0,
1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0,
1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0,
0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1,
1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0,
1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1,
1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1,
0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0,
1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1,
0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0,
0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0,
0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1,
1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0,
0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1,
1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0,
0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1,
0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0,
1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1,
1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0,
0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1,
1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1,
1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1,
0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1,
1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1,
1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0,
1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1,
1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1,
0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1,
0, 1,
];
let bm = render_cws(&cws_c4, metric_c4).unwrap();
assert_eq!(bm.width(), 99);
assert_eq!(bm.height(), 6);
let mut got: Vec<u8> = Vec::with_capacity(bm.width() * bm.height());
for y in 0..bm.height() {
for x in 0..bm.width() {
got.push(bm.get(x, y) as u8);
}
}
assert_eq!(got, want_pixs);
}
#[test]
fn raps_table_endpoints() {
assert_eq!(RAPS[0][0], 802);
assert_eq!(RAPS[0][51], 866);
assert_eq!(RAPS[1][0], 718);
assert_eq!(RAPS[1][51], 734);
}
#[test]
fn pack_ccb_datcws_matches_bwip_js_oracle() {
let (_linear, comp) = crate::symbology::composite::split_composite_input(
"(01)24012345678905|(10)BATCH(21)SERIAL1234567(91)EXTRADATAFORCC",
)
.unwrap();
let cc = crate::symbology::gs1_cc::encode_cc(comp, 4).unwrap();
assert_eq!(cc.version, crate::symbology::gs1_cc::CcVersion::B);
let bytes: Vec<u8> = cc.codewords.iter().map(|&v| v as u8).collect();
let datcws = pack_ccb_datcws(&bytes);
let want: [u16; 30] = [
920, 901, 295, 741, 122, 515, 891, 327, 50, 671, 298, 425, 143, 546, 297, 824, 583,
347, 49, 198, 652, 85, 313, 487, 186, 25, 510, 16, 132, 33,
];
assert_eq!(datcws, want.to_vec(), "CC-B datcws mismatch vs bwip-js");
}
#[test]
fn pack_ccb_datcws_narrow_arithmetic() {
assert_eq!(pack_ccb_datcws(&[]), vec![920u16, 924]);
assert_eq!(pack_ccb_datcws(&[42]), vec![920u16, 901, 42]);
assert_eq!(
pack_ccb_datcws(&[0, 0, 0, 0, 0, 0]),
vec![920u16, 924, 0, 0, 0, 0, 0]
);
assert_eq!(
pack_ccb_datcws(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
vec![920u16, 924, 429, 11, 71, 222, 855]
);
let only_msb = pack_ccb_datcws(&[1, 0, 0, 0, 0, 0]);
let only_lsb = pack_ccb_datcws(&[0, 0, 0, 0, 0, 1]);
assert_ne!(only_msb, only_lsb, "byte 0 vs byte 5 must differ");
assert_eq!(only_lsb, vec![920u16, 924, 0, 0, 0, 0, 1]);
assert_eq!(
pack_ccb_datcws(&[0, 0, 0, 0, 0, 0, 42]),
vec![920u16, 901, 0, 0, 0, 0, 0, 42]
);
}
#[test]
fn render_ccb_cws_matches_bwip_js_after_rs_ecc() {
let (_, comp) = crate::symbology::composite::split_composite_input(
"(01)24012345678905|(10)BATCH(21)SERIAL1234567(91)EXTRADATAFORCC",
)
.unwrap();
let cc = crate::symbology::gs1_cc::encode_cc(comp, 4).unwrap();
let bytes: Vec<u8> = cc.codewords.iter().map(|&v| v as u8).collect();
let (cws, metric) = ccb_cws_compose(&bytes, 4).unwrap();
assert_eq!(metric, (4, 12, 18, 25, 25, 25));
let want_check: [u16; 18] = [
548, 371, 837, 211, 271, 234, 345, 751, 858, 512, 887, 804, 631, 488, 531, 153, 23, 116,
];
assert_eq!(&cws[30..48], &want_check, "RS-ECC mismatch vs bwip-js");
assert_eq!(cws.len(), 48);
}
#[test]
fn encode_ccb_matches_bwip_js_corpus() {
type CcbRow = (&'static str, &'static [u16], &'static [u16], (u8, u8, u8));
let golden: &[CcbRow] = &[
(
"A",
&[920, 901, 65],
&[920, 901, 65, 900, 104, 433, 214, 488, 430, 798, 54],
(1, 11, 7),
),
(
"ABC",
&[920, 901, 65, 66, 67],
&[
920, 901, 65, 66, 67, 900, 900, 627, 760, 860, 307, 924, 236, 543,
],
(1, 14, 7),
),
(
"ABCDE",
&[920, 901, 65, 66, 67, 68, 69],
&[
920, 901, 65, 66, 67, 68, 69, 196, 173, 108, 78, 603, 262, 767,
],
(1, 14, 7),
),
(
"ABCDEF",
&[920, 924, 109, 326, 368, 127, 330],
&[
920, 924, 109, 326, 368, 127, 330, 543, 96, 102, 553, 316, 639, 822,
],
(1, 14, 7),
),
(
"ABCDEFG",
&[920, 901, 109, 326, 368, 127, 330, 71],
&[
920, 901, 109, 326, 368, 127, 330, 71, 900, 900, 135, 863, 777, 878, 861, 819,
293,
],
(1, 17, 7),
),
(
"ABCDEFGHIJKL",
&[920, 924, 109, 326, 368, 127, 330, 119, 411, 338, 47, 816],
&[
920, 924, 109, 326, 368, 127, 330, 119, 411, 338, 47, 816, 232, 220, 183, 180,
681, 376, 562, 810,
],
(1, 20, 8),
),
(
"ABCDEFGHIJKLMNOP",
&[
920, 901, 109, 326, 368, 127, 330, 119, 411, 338, 47, 816, 77, 78, 79, 80,
],
&[
920, 901, 109, 326, 368, 127, 330, 119, 411, 338, 47, 816, 77, 78, 79, 80, 142,
207, 419, 668, 743, 726, 848, 615,
],
(1, 24, 8),
),
(
"ABCDEFGHIJKLMNOPQRSTUVWX",
&[
920, 924, 109, 326, 368, 127, 330, 119, 411, 338, 47, 816, 129, 496, 307, 868,
402, 139, 581, 277, 788, 888,
],
&[
920, 924, 109, 326, 368, 127, 330, 119, 411, 338, 47, 816, 129, 496, 307, 868,
402, 139, 581, 277, 788, 888, 900, 900, 381, 346, 202, 85, 160, 435, 498, 254,
657, 0,
],
(2, 17, 10),
),
];
for (text, want_datcws, want_cws, want_crk) in golden {
let datcws = pack_ccb_datcws(text.as_bytes());
assert_eq!(&datcws, want_datcws, "datcws mismatch for {text:?}");
let (cws, metric) = ccb_cws_compose(text.as_bytes(), 0).unwrap();
assert_eq!(&cws, want_cws, "cws mismatch for {text:?}");
let (c, r, k, _, _, _) = metric;
assert_eq!(
(c, r, k),
*want_crk,
"metric (c, r, k) mismatch for {text:?}",
);
let bm = encode(text, &Options::default().with("ccb", "true"))
.unwrap_or_else(|e| panic!("encode failed for {text:?}: {e:?}"));
assert!(
bm.width() > 0 && bm.height() > 0,
"empty matrix for {text:?}",
);
}
}
#[test]
fn encode_ccb_false_equivalent_to_default() {
let a = encode("PDF417", &Options::default()).unwrap();
let b = encode("PDF417", &Options::default().with("ccb", "false")).unwrap();
assert_eq!(a.width(), b.width());
assert_eq!(a.height(), b.height());
for y in 0..a.height() {
for x in 0..a.width() {
assert_eq!(a.get(x, y), b.get(x, y), "pixel mismatch at ({x},{y})");
}
}
}
#[test]
fn encode_ccb_rejects_invalid_value() {
let err = encode("A", &Options::default().with("ccb", "maybe")).unwrap_err();
match err {
Error::InvalidOption(msg) => {
assert!(
msg.contains("micropdf417:"),
"must carry the micropdf417 prefix; got {msg:?}"
);
assert!(
msg.contains("ccb=\"maybe\""),
"must Debug-echo the offending value; got {msg:?}"
);
assert!(
msg.contains("must be"),
"must carry the predicate; got {msg:?}"
);
assert!(
msg.contains("\"true\"") && msg.contains("\"false\""),
"must name BOTH valid values; got {msg:?}"
);
assert!(
!msg.contains("cca="),
"ccb diagnostic must NOT leak cca-option wording; got {msg:?}"
);
}
other => panic!("expected InvalidOption, got {other:?}"),
}
}
#[test]
fn encode_cca_matches_bwip_js_corpus() {
type CcaRow = (&'static str, &'static [u32], &'static [u16], (u8, u8, u8));
let golden: &[CcaRow] = &[
(
"^000^123^456^789^900",
&[0, 123, 456, 789, 900],
&[0, 123, 456, 789, 900, 900, 904, 445, 644, 131],
(2, 5, 4),
),
(
"^001^002^003^004^005^006",
&[1, 2, 3, 4, 5, 6],
&[1, 2, 3, 4, 5, 6, 820, 514, 788, 278],
(2, 5, 4),
),
(
"^010^020^030^040^050^060^070",
&[10, 20, 30, 40, 50, 60, 70],
&[10, 20, 30, 40, 50, 60, 70, 900, 298, 635, 657, 263],
(2, 6, 4),
),
(
"^100^200^300^400^500^600^700^800^900^001^002^003",
&[100, 200, 300, 400, 500, 600, 700, 800, 900, 1, 2, 3],
&[
100, 200, 300, 400, 500, 600, 700, 800, 900, 1, 2, 3, 844, 726, 417, 104, 183,
168,
],
(2, 9, 6),
),
(
"^928^000^928^000^928",
&[928, 0, 928, 0, 928],
&[928, 0, 928, 0, 928, 900, 363, 554, 450, 893],
(2, 5, 4),
),
];
for (text, want_datcws, want_cws, want_crk) in golden {
let datcws = parse_raw_codewords(text.as_bytes())
.unwrap_or_else(|e| panic!("parse_raw_codewords({text:?}) failed: {e:?}"));
assert_eq!(&datcws, want_datcws, "datcws mismatch for {text:?}");
let (bm, metric) = render_cca(want_datcws, 0)
.unwrap_or_else(|e| panic!("render_cca({text:?}) failed: {e}"));
let (c, r, k, _, _, _) = metric;
assert_eq!((c, r, k), *want_crk, "metric mismatch for {text:?}",);
let n = (c as usize) * (r as usize) - (k as usize);
let mut cws: Vec<u16> = want_datcws.iter().map(|&v| v as u16).collect();
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k as usize);
cws.extend_from_slice(&check);
assert_eq!(&cws, want_cws, "cws mismatch for {text:?}");
let bm_public = encode(text, &Options::default().with("cca", "true"))
.unwrap_or_else(|e| panic!("encode failed for {text:?}: {e:?}"));
assert_eq!(bm.width(), bm_public.width(), "width mismatch for {text:?}",);
assert_eq!(
bm.height(),
bm_public.height(),
"height mismatch for {text:?}",
);
}
}
#[test]
fn encode_cca_false_equivalent_to_default() {
let a = encode("PDF417", &Options::default()).unwrap();
let b = encode("PDF417", &Options::default().with("cca", "false")).unwrap();
assert_eq!(a.width(), b.width());
assert_eq!(a.height(), b.height());
}
#[test]
fn encode_cca_rejects_invalid_value() {
let err = encode("^001", &Options::default().with("cca", "maybe")).unwrap_err();
match err {
Error::InvalidOption(msg) => {
assert!(
msg.contains("micropdf417:"),
"must carry the micropdf417 prefix; got {msg:?}"
);
assert!(
msg.contains("cca=\"maybe\""),
"must Debug-echo the offending value; got {msg:?}"
);
assert!(
msg.contains("must be"),
"must carry the predicate; got {msg:?}"
);
assert!(
msg.contains("\"true\"") && msg.contains("\"false\""),
"must name BOTH valid values; got {msg:?}"
);
assert!(
!msg.contains("ccb="),
"cca diagnostic must NOT leak ccb-option wording; got {msg:?}"
);
}
other => panic!("expected InvalidOption, got {other:?}"),
}
}
#[test]
fn encode_cca_and_ccb_combined_rejected() {
let err = encode(
"^001",
&Options::default().with("cca", "true").with("ccb", "true"),
)
.unwrap_err();
match err {
Error::InvalidOption(msg) => assert!(
msg.contains("cca") && msg.contains("ccb"),
"msg was {msg:?}",
),
other => panic!("expected InvalidOption, got {other:?}"),
}
}
#[test]
fn parse_raw_codewords_happy_path_arithmetic() {
assert_eq!(parse_raw_codewords(b"").unwrap(), Vec::<u32>::new());
assert_eq!(parse_raw_codewords(b"^000").unwrap(), vec![0]);
assert_eq!(parse_raw_codewords(b"^123").unwrap(), vec![123]);
assert_eq!(parse_raw_codewords(b"^928").unwrap(), vec![928]);
let err_929 = parse_raw_codewords(b"^929").unwrap_err();
assert!(
err_929.contains("must be 0..=928"),
"boundary+1 diagnostic must carry the bound; got {err_929:?}"
);
assert!(
err_929.contains("929"),
"boundary+1 diagnostic must echo the offending value 929; got {err_929:?}"
);
assert_eq!(
parse_raw_codewords(b"^001^002^900").unwrap(),
vec![1, 2, 900]
);
assert_eq!(parse_raw_codewords(b"^900^928").unwrap(), vec![900, 928]);
assert_eq!(
parse_raw_codewords(b"^321").unwrap(),
vec![321],
"MSB-first base-10 accumulator (rejects `+` instead of `*`)"
);
}
#[test]
fn parse_raw_codewords_rejects_malformed() {
let err = parse_raw_codewords(b"AAA0").unwrap_err();
assert!(
err.contains("micropdf417:") && err.contains("raw codewords"),
"bad-prefix diagnostic must carry the symbology + 'raw codewords' tags; got {err:?}"
);
assert!(
err.contains("expected `^` at offset 0"),
"bad-prefix diagnostic must call out the missing caret + offset; got {err:?}"
);
assert!(
err.contains("0x41"),
"bad-prefix diagnostic must echo the offending byte 0x41 ('A'); got {err:?}"
);
let err = parse_raw_codewords(b"^00X").unwrap_err();
assert!(
err.contains("non-digit"),
"non-digit-segment diagnostic must say 'non-digit'; got {err:?}"
);
assert!(
err.contains("0x58"),
"non-digit-segment diagnostic must echo the offending byte 0x58 ('X'); got {err:?}"
);
assert!(
err.contains("at offset 3"),
"non-digit-segment diagnostic must echo the offset (3); got {err:?}"
);
assert!(
!err.contains("must be 0..=928"),
"non-digit-segment diagnostic must not leak the value-range arm; got {err:?}"
);
let err = parse_raw_codewords(b"^929").unwrap_err();
assert!(
err.contains("must be 0..=928"),
"value-range diagnostic must carry the 0..=928 bound; got {err:?}"
);
assert!(
err.contains("929"),
"value-range diagnostic must echo the offending value 929; got {err:?}"
);
assert!(
!err.contains("non-digit") && !err.contains("expected `^`"),
"value-range diagnostic must not leak other arms' tags; got {err:?}"
);
let err = parse_raw_codewords(b"^001^00").unwrap_err();
assert!(
err.contains("trailing"),
"trailing-partial diagnostic must say 'trailing'; got {err:?}"
);
assert!(
err.contains("3 trailing byte(s)"),
"trailing-partial diagnostic must echo the count (3); got {err:?}"
);
assert!(
err.contains("at offset 4"),
"trailing-partial diagnostic must echo the offset (4); got {err:?}"
);
}
#[test]
fn encode_raw_matches_bwip_js_corpus() {
type RawRow = (&'static str, &'static [u32], &'static [u16], (u8, u8, u8));
let golden: &[RawRow] = &[
(
"^900",
&[900],
&[900, 900, 900, 900, 78, 541, 601, 125, 9, 181, 356],
(1, 11, 7),
),
(
"^902^123",
&[902, 123],
&[902, 123, 900, 900, 452, 808, 48, 873, 256, 59, 878],
(1, 11, 7),
),
(
"^001^002^003^004",
&[1, 2, 3, 4],
&[1, 2, 3, 4, 650, 91, 697, 750, 240, 109, 321],
(1, 11, 7),
),
(
"^001^002^003^004^005",
&[1, 2, 3, 4, 5],
&[1, 2, 3, 4, 5, 900, 900, 474, 434, 104, 515, 563, 606, 586],
(1, 14, 7),
),
(
"^001^002^003^004^005^006^007^008",
&[1, 2, 3, 4, 5, 6, 7, 8],
&[
1, 2, 3, 4, 5, 6, 7, 8, 900, 900, 147, 13, 316, 647, 695, 78, 212,
],
(1, 17, 7),
),
(
"^000^928^000^928",
&[0, 928, 0, 928],
&[0, 928, 0, 928, 267, 578, 316, 264, 921, 33, 539],
(1, 11, 7),
),
];
for (text, want_datcws, want_cws, want_crk) in golden {
let datcws = parse_raw_codewords(text.as_bytes())
.unwrap_or_else(|e| panic!("parse_raw_codewords({text:?}) failed: {e:?}"));
assert_eq!(&datcws, want_datcws, "datcws mismatch for {text:?}");
let metric = select_metric(want_datcws.len()).unwrap_or_else(|| {
panic!(
"select_metric(len={}) returned None for {text:?}",
want_datcws.len()
)
});
let (c, r, k, _, _, _) = metric;
assert_eq!((c, r, k), *want_crk, "metric mismatch for {text:?}");
let n = (c as usize) * (r as usize) - (k as usize);
let mut cws: Vec<u16> = want_datcws.iter().map(|&v| v as u16).collect();
cws.resize(n, 900);
let check = crate::util::rs_gf929::encode(&cws[..n], k as usize);
cws.extend_from_slice(&check);
assert_eq!(&cws, want_cws, "cws mismatch for {text:?}");
let bm = encode(text, &Options::default().with("raw", "true"))
.unwrap_or_else(|e| panic!("encode failed for {text:?}: {e:?}"));
assert!(
bm.width() > 0 && bm.height() > 0,
"empty matrix for {text:?}",
);
}
}
#[test]
fn encode_raw_false_equivalent_to_default() {
let a = encode("PDF417", &Options::default()).unwrap();
let b = encode("PDF417", &Options::default().with("raw", "false")).unwrap();
assert_eq!(a.width(), b.width());
assert_eq!(a.height(), b.height());
}
#[test]
fn encode_raw_rejects_invalid_value() {
let err = encode("^001", &Options::default().with("raw", "maybe")).unwrap_err();
match err {
Error::InvalidOption(msg) => {
assert!(
msg.contains("micropdf417:"),
"must carry the micropdf417 prefix; got {msg:?}"
);
assert!(
msg.contains("raw=\"maybe\""),
"must Debug-echo the offending value; got {msg:?}"
);
assert!(
msg.contains("must be"),
"must carry the predicate; got {msg:?}"
);
assert!(
msg.contains("\"true\"") && msg.contains("\"false\""),
"must name BOTH valid values; got {msg:?}"
);
assert!(
!msg.contains("cca=") && !msg.contains("ccb="),
"raw diagnostic must NOT leak cca/ccb option wording; got {msg:?}"
);
}
other => panic!("expected InvalidOption, got {other:?}"),
}
}
#[test]
fn encode_raw_combined_with_cca_or_ccb_rejected() {
for combine in ["cca", "ccb"] {
let err = encode(
"^001",
&Options::default().with("raw", "true").with(combine, "true"),
)
.unwrap_err();
let Error::InvalidOption(msg) = err else {
panic!("expected InvalidOption for raw+{combine}; got {err:?}");
};
assert!(
msg.contains("micropdf417:"),
"diagnostic for raw+{combine} must carry micropdf417 prefix; got {msg:?}"
);
assert!(
msg.contains("raw=true cannot be combined with cca=true or ccb=true"),
"diagnostic for raw+{combine} must carry the full combine guard message; got {msg:?}"
);
assert!(
!msg.contains("must be \"true\" or \"false\""),
"diagnostic for raw+{combine} must not leak the value-parse template; got {msg:?}"
);
}
}
#[test]
fn parse_text_escapes_substitutes_correctly() {
assert_eq!(parse_text_escapes(b"^065BC").unwrap(), b"ABC");
assert_eq!(parse_text_escapes(b"^065^066^067").unwrap(), b"ABC");
assert_eq!(parse_text_escapes(b"^255AB").unwrap(), &[255, b'A', b'B']);
assert_eq!(parse_text_escapes(b"X^TABY").unwrap(), &[b'X', 9, b'Y']);
assert_eq!(parse_text_escapes(b"X^CRY").unwrap(), &[b'X', 13, b'Y']);
assert_eq!(parse_text_escapes(b"^NUL!").unwrap(), &[0, b'!']);
assert_eq!(parse_text_escapes(b"ABC").unwrap(), b"ABC");
}
#[test]
fn parse_text_escapes_rejects_ordinal_above_255() {
for (input, want_value) in [(b"^256AB" as &[u8], "256"), (b"^999X", "999")] {
let err = parse_text_escapes(input).unwrap_err();
assert!(
err.contains("micropdf417 parse:"),
"diagnostic for {input:?} must carry the symbology+mode prefix; got {err:?}"
);
assert!(
err.contains("ordinal must be 000..=255"),
"diagnostic for {input:?} must carry the 000..=255 bound text; got {err:?}"
);
assert!(
err.contains(want_value),
"diagnostic for {input:?} must echo the offending value {want_value:?}; got {err:?}"
);
}
assert_eq!(
parse_text_escapes(b"^255").unwrap(),
vec![255u8],
"^255 must encode as byte 0xFF; kills `>= 255` off-by-one"
);
}
#[test]
fn encode_parse_matches_baseline_text_encode() {
let cases: &[(&str, &[u8])] = &[
("ABC", b"ABC"),
("^065BC", b"ABC"),
("^065^066^067", b"ABC"),
("^255AB", &[255, b'A', b'B']),
("X^TABY", &[b'X', 9, b'Y']),
("X^CRY", &[b'X', 13, b'Y']),
];
for (text, want_bytes) in cases {
let substituted = parse_text_escapes(text.as_bytes()).unwrap();
assert_eq!(
&substituted, want_bytes,
"substitution mismatch for {text:?}"
);
let via_parse = encode(text, &Options::default().with("parse", "true"))
.unwrap_or_else(|e| panic!("parse=true encode failed for {text:?}: {e:?}"));
let direct = render(want_bytes)
.unwrap_or_else(|e| panic!("render({want_bytes:?}) baseline failed: {e:?}"));
assert_eq!(
via_parse.width(),
direct.width(),
"width mismatch for {text:?}"
);
assert_eq!(
via_parse.height(),
direct.height(),
"height mismatch for {text:?}",
);
for y in 0..direct.height() {
for x in 0..direct.width() {
assert_eq!(
via_parse.get(x, y),
direct.get(x, y),
"pixel mismatch at ({x},{y}) for {text:?}",
);
}
}
}
}
#[test]
fn encode_parse_false_equivalent_to_default() {
let a = encode("PDF417", &Options::default()).unwrap();
let b = encode("PDF417", &Options::default().with("parse", "false")).unwrap();
assert_eq!(a.width(), b.width());
assert_eq!(a.height(), b.height());
}
#[test]
fn encode_parse_rejects_invalid_value() {
let err = encode("ABC", &Options::default().with("parse", "maybe")).unwrap_err();
match err {
Error::InvalidOption(msg) => {
assert!(
msg.contains("micropdf417:"),
"must carry the micropdf417 prefix; got {msg:?}"
);
assert!(
msg.contains("parse=\"maybe\""),
"must Debug-echo as parse= (NOT parsefnc=); got {msg:?}"
);
assert!(
msg.contains("must be"),
"must carry the predicate; got {msg:?}"
);
assert!(
msg.contains("\"true\"") && msg.contains("\"false\""),
"must name BOTH valid values; got {msg:?}"
);
assert!(
!msg.contains("parsefnc="),
"parse diagnostic must NOT leak parsefnc-option wording; got {msg:?}"
);
}
other => panic!("expected InvalidOption, got {other:?}"),
}
}
#[test]
fn parse_text_parsefnc_handles_eci_and_caret_escape() {
let (eci, body) = parse_text_parsefnc(b"PDF417").unwrap();
assert!(
eci.is_empty(),
"parse_text_parsefnc(b\"PDF417\") (plain text, no ^ECI marker) must produce empty eci vec; got {eci:?}"
);
assert_eq!(body, b"PDF417");
let (eci, body) = parse_text_parsefnc(b"FOO^^BAR").unwrap();
assert!(
eci.is_empty(),
"parse_text_parsefnc(b\"FOO^^BAR\") (caret-escape ^^ → literal '^', no ^ECI prefix) must produce empty eci vec; got {eci:?}"
);
assert_eq!(body, b"FOO^BAR");
let (eci, body) = parse_text_parsefnc(b"^ECI000026ABC").unwrap();
assert_eq!(eci, vec![927, 26]);
assert_eq!(body, b"ABC");
let (eci, body) = parse_text_parsefnc(b"^ECI0000091234").unwrap();
assert_eq!(eci, vec![927, 9]);
assert_eq!(body, b"1234");
let (eci, body) = parse_text_parsefnc(b"^ECI000003HELLO").unwrap();
assert_eq!(eci, vec![927, 3]);
assert_eq!(body, b"HELLO");
}
#[test]
fn parse_text_parsefnc_rejects_bad_eci() {
let err = parse_text_parsefnc(b"^ECIabcdef").unwrap_err();
assert!(
err.contains("micropdf417 parsefnc:"),
"non-digit-ECI diagnostic must carry the symbology+mode prefix; got {err:?}"
);
assert!(
err.contains("^ECI must be followed by 6 ASCII digits"),
"non-digit-ECI diagnostic must carry the 6-digit-requirement text; got {err:?}"
);
assert!(
err.contains("\"abcdef\""),
"non-digit-ECI diagnostic must echo the offending body via {{:?}}; got {err:?}"
);
assert!(
!err.contains("mid-stream"),
"non-digit-ECI diagnostic must not leak the mid-stream arm; got {err:?}"
);
let err = parse_text_parsefnc(b"ABC^ECI000003").unwrap_err();
assert!(
err.contains("mid-stream ^ECI"),
"mid-stream diagnostic must call out 'mid-stream ^ECI'; got {err:?}"
);
assert!(
err.contains("place ECI prefixes only at the start"),
"mid-stream diagnostic must carry the placement hint; got {err:?}"
);
assert!(
!err.contains("6 ASCII digits"),
"mid-stream diagnostic must not leak the non-digit-ECI arm; got {err:?}"
);
}
#[test]
fn encode_parsefnc_matches_bwip_js_corpus() {
let cases: &[(&str, &[u16])] = &[
("FOO^^BAR", &[900, 164, 448, 748, 30, 539]),
("^ECI000026ABC", &[927, 26, 901, 65, 66, 67]),
("^ECI0000091234", &[927, 9, 901, 49, 50, 51, 52]),
("PDF417", &[900, 453, 178, 121, 239]),
("^ECI000003HELLO", &[927, 3, 900, 214, 341, 449]),
];
for (text, want_datcws) in cases {
let (eci, body) = parse_text_parsefnc(text.as_bytes())
.unwrap_or_else(|e| panic!("parse_text_parsefnc({text:?}) failed: {e:?}"));
let mut datcws: Vec<u16> = Vec::new();
datcws.extend_from_slice(&eci);
datcws.extend_from_slice(
&data_codewords(&body)
.unwrap_or_else(|e| panic!("data_codewords(body for {text:?}) failed: {e:?}")),
);
assert_eq!(&datcws, want_datcws, "datcws mismatch for {text:?}");
let bm = encode(text, &Options::default().with("parsefnc", "true"))
.unwrap_or_else(|e| panic!("encode failed for {text:?}: {e:?}"));
assert!(
bm.width() > 0 && bm.height() > 0,
"empty matrix for {text:?}",
);
}
}
#[test]
fn encode_parsefnc_false_equivalent_to_default() {
let a = encode("PDF417", &Options::default()).unwrap();
let b = encode("PDF417", &Options::default().with("parsefnc", "false")).unwrap();
assert_eq!(a.width(), b.width());
assert_eq!(a.height(), b.height());
}
#[test]
fn encode_parsefnc_rejects_invalid_value() {
let err = encode("ABC", &Options::default().with("parsefnc", "maybe")).unwrap_err();
match err {
Error::InvalidOption(msg) => {
assert!(
msg.contains("micropdf417:"),
"must carry the micropdf417 prefix; got {msg:?}"
);
assert!(
msg.contains("parsefnc=\"maybe\""),
"must Debug-echo as parsefnc=; got {msg:?}"
);
assert!(
msg.contains("must be"),
"must carry the predicate; got {msg:?}"
);
assert!(
msg.contains("\"true\"") && msg.contains("\"false\""),
"must name BOTH valid values; got {msg:?}"
);
}
other => panic!("expected InvalidOption, got {other:?}"),
}
}
#[test]
fn encode_parsefnc_rejects_midstream_eci() {
let err = encode(
"ABC^ECI000003DEF",
&Options::default().with("parsefnc", "true"),
)
.unwrap_err();
let Error::InvalidData(msg) = err else {
panic!("expected InvalidData for mid-stream ^ECI; got {err:?}");
};
assert!(
msg.contains("mid-stream ^ECI"),
"diagnostic must call out 'mid-stream ^ECI'; got {msg:?}"
);
assert!(
msg.contains("not supported"),
"diagnostic must say the marker is not supported; got {msg:?}"
);
assert!(
msg.contains("place ECI prefixes only at the start"),
"diagnostic must include the placement hint; got {msg:?}"
);
assert!(
msg.contains("micropdf417"),
"diagnostic must carry the symbology tag; got {msg:?}"
);
}
#[test]
fn append_bits_msb_first_packing() {
let mut out: Vec<u8> = vec![5, 6];
append_bits(&mut out, 0xFF, 0);
assert_eq!(out, vec![5, 6], "n=0 → no change");
let mut out: Vec<u8> = Vec::new();
append_bits(&mut out, 0, 4);
assert_eq!(out, vec![0, 0, 0, 0]);
let mut out: Vec<u8> = Vec::new();
append_bits(&mut out, 0b1010, 4);
assert_eq!(out, vec![1, 0, 1, 0], "MSB-first: 0b1010 → [1,0,1,0]");
let mut out: Vec<u8> = Vec::new();
append_bits(&mut out, 0b11111, 5);
assert_eq!(out, vec![1, 1, 1, 1, 1]);
let mut out: Vec<u8> = Vec::new();
append_bits(&mut out, 1, 1);
assert_eq!(out, vec![1]);
let mut out: Vec<u8> = vec![9, 9];
append_bits(&mut out, 0b101, 3);
assert_eq!(
out,
vec![9, 9, 1, 0, 1],
"append_bits must extend, not replace"
);
let mut out: Vec<u8> = Vec::new();
append_bits(&mut out, 0xAB, 8);
assert_eq!(out, vec![1, 0, 1, 0, 1, 0, 1, 1]);
let mut out: Vec<u8> = Vec::new();
append_bits(&mut out, 0x12345, 17);
assert_eq!(
out,
vec![1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1],
"17-bit pack matches MicroPDF417 codeword width"
);
}
#[test]
fn parse_version_string_happy_path() {
assert_eq!(parse_version_string("14x1").unwrap(), (14, 1));
assert_eq!(parse_version_string("20x1").unwrap(), (20, 1));
assert_eq!(parse_version_string("11x2").unwrap(), (11, 2));
assert_eq!(parse_version_string("44x4").unwrap(), (44, 4));
}
#[test]
fn parse_version_string_rejects_malformed() {
for (input, arm) in [
("11", "no 'x' separator"),
("11x", "empty cols"),
("xN", "empty rows"),
("Ax1", "non-digit rows"),
("11xC", "non-digit cols"),
("11x1x1", "extra 'x' in cols → non-digit"),
] {
let err = parse_version_string(input).unwrap_err();
let Error::InvalidOption(msg) = err else {
panic!("{input:?} ({arm}) must yield InvalidOption; got {err:?}");
};
assert!(
msg.contains("micropdf417:"),
"{input:?} ({arm}) diagnostic must carry symbology prefix; got {msg:?}"
);
assert!(
msg.contains("must be formatted as RxC"),
"{input:?} ({arm}) must surface the RxC-format diagnostic; got {msg:?}"
);
assert!(
!msg.contains("out of range"),
"{input:?} ({arm}) must not leak the overflow diagnostic; got {msg:?}"
);
}
let err = parse_version_string("999x9").unwrap_err();
let Error::InvalidOption(msg) = err else {
panic!("'999x9' must yield InvalidOption; got {err:?}");
};
assert!(
msg.contains("rows component out of range"),
"'999x9' must hit the rows-overflow arm; got {msg:?}"
);
assert!(
!msg.contains("columns component"),
"'999x9' must not leak the cols-overflow text; got {msg:?}"
);
let err = parse_version_string("9x999").unwrap_err();
let Error::InvalidOption(msg) = err else {
panic!("'9x999' must yield InvalidOption; got {err:?}");
};
assert!(
msg.contains("columns component out of range"),
"'9x999' must hit the cols-overflow arm; got {msg:?}"
);
assert!(
!msg.contains("rows component"),
"'9x999' must not leak the rows-overflow text; got {msg:?}"
);
}
#[test]
fn encode_with_size_constraints_matches_bwip_js() {
type SizeCase = (
&'static str,
&'static [(&'static str, &'static str)],
(u8, u8, u8),
);
let cases: &[SizeCase] = &[
("ABC", &[("version", "14x1")], (1, 14, 7)),
("ABC", &[("version", "20x1")], (1, 20, 8)),
("ABCDEFGH", &[("version", "11x2")], (2, 11, 9)),
("ABCDEFGH", &[("version", "14x2")], (2, 14, 9)),
("ABCDEFGH", &[("columns", "2")], (2, 8, 8)),
("ABC", &[("rows", "14")], (1, 14, 7)),
("ABCDEFGH", &[("columns", "2"), ("rows", "11")], (2, 11, 9)),
];
for (text, kvs, want_crk) in cases {
let mut opts = Options::default();
for (k, v) in *kvs {
opts = opts.with(*k, *v);
}
let datcws = data_codewords(text.as_bytes()).unwrap();
let (_, size) = check_micropdf417_opts(&opts).unwrap();
let metric = select_metric_constrained(datcws.len(), size.ucols, size.urows).unwrap();
let (c, r, k, _, _, _) = metric;
assert_eq!((c, r, k), *want_crk, "metric mismatch for {text:?} {kvs:?}");
let bm = encode(text, &opts)
.unwrap_or_else(|e| panic!("encode failed for {text:?} with {kvs:?}: {e:?}"));
assert!(bm.width() > 0 && bm.height() > 0);
}
}
#[test]
fn encode_rejects_invalid_version() {
let err = encode("ABC", &Options::default().with("version", "garbage")).unwrap_err();
let Error::InvalidOption(msg) = err else {
panic!("expected InvalidOption for version=\"garbage\"; got {err:?}");
};
assert!(
msg.contains("micropdf417:"),
"diagnostic must carry the symbology prefix; got {msg:?}"
);
assert!(
msg.contains("\"garbage\""),
"diagnostic must echo the offending spec; got {msg:?}"
);
assert!(
msg.contains("must be formatted as RxC"),
"diagnostic must carry the RxC-format tail; got {msg:?}"
);
assert!(
!msg.contains("out of range") && !msg.contains("cca") && !msg.contains("raw="),
"version=garbage must short-circuit before other option arms; got {msg:?}"
);
}
#[test]
fn encode_with_oversized_constraint_errors() {
let err = encode(
"ABCDEFGHIJKLMNOPQRSTUVWX",
&Options::default().with("version", "11x1"),
)
.unwrap_err();
let Error::InvalidData(msg) = err else {
panic!("expected InvalidData for oversized constraint; got {err:?}");
};
assert!(
msg.contains("MicroPDF417:"),
"diagnostic must carry the symbology prefix; got {msg:?}"
);
assert!(
msg.contains("no metric for"),
"diagnostic must call out 'no metric for'; got {msg:?}"
);
assert!(
msg.contains("datcws"),
"diagnostic must tag the codeword unit (datcws); got {msg:?}"
);
assert!(
msg.contains("columns=1"),
"diagnostic must echo the requested columns constraint; got {msg:?}"
);
assert!(
msg.contains("rows=11"),
"diagnostic must echo the requested rows constraint; got {msg:?}"
);
assert!(
!msg.contains("^ECI") && !msg.contains("ordinal") && !msg.contains("trailing"),
"no-metric diagnostic must not leak parser-stage arms; got {msg:?}"
);
}
#[test]
fn select_metric_smallest_fit() {
let m1 = select_metric(1).expect(
"select_metric(1) must yield Some(metric) — smallest payload must fit smallest metric",
);
let m2 = select_metric(1).expect(
"select_metric(1) called twice must yield Some(metric) — determinism check requires non-None on repeat",
);
assert_eq!(m1, m2, "select_metric must be deterministic");
let (c, r, k, _, _, _) = m1;
let capacity = (c as usize) * (r as usize) - (k as usize);
assert!(capacity >= 1, "metric capacity {capacity} < 1");
if let Some(big) = select_metric(50) {
let (c, r, k, _, _, _) = big;
let big_cap = (c as usize) * (r as usize) - (k as usize);
assert!(big_cap >= 50, "metric for 50 cws has cap {big_cap}");
}
assert_eq!(select_metric(usize::MAX), None);
}
#[test]
fn select_metric_constrained_respects_constraints() {
assert_eq!(select_metric_constrained(1, 0, 0), select_metric(1));
if let Some(m) = select_metric_constrained(1, 2, 0) {
assert_eq!(m.0, 2, "ucols=2 returned metric with cols {}", m.0);
}
if let Some(m) = select_metric_constrained(1, 3, 0) {
assert_eq!(m.0, 3);
}
if let Some(m) = select_metric_constrained(1, 4, 0) {
assert_eq!(m.0, 4);
}
if let Some(m) = select_metric_constrained(1, 0, 5) {
assert_eq!(m.1, 5);
}
}
#[test]
fn select_ccb_metric_respects_column_constraint() {
let m = select_ccb_metric(5, 0).expect("5 cws should fit");
assert_eq!(m, (1, 14, 7, 8, 0, 8));
let m = select_ccb_metric(5, 2).expect("5 cws should fit in c=2");
assert_eq!(m.0, 2, "ucols=2 must return c=2 metric");
assert_eq!(m, (2, 8, 8, 1, 0, 1));
let m = select_ccb_metric(5, 3).expect("5 cws fits c=3");
assert_eq!(m.0, 3);
let m = select_ccb_metric(5, 4).expect("5 cws fits c=4");
assert_eq!(m.0, 4);
assert_eq!(
select_ccb_metric(200, 0),
None,
"200 cws should exceed largest NONCCA capacity (176)"
);
assert_eq!(
select_ccb_metric(50, 2),
None,
"50 cws should not fit any c=2 metric (max 37)"
);
}
#[test]
fn parse_version_string_all_branches() {
assert_eq!(parse_version_string("11x2").unwrap(), (11, 2));
assert_eq!(parse_version_string("4x44").unwrap(), (4, 44));
let err = parse_version_string("114").unwrap_err();
assert!(
matches!(&err, Error::InvalidOption(m) if m.contains("RxC")),
"no-separator: got {err:?}",
);
let err = parse_version_string("x4").unwrap_err();
assert!(
matches!(&err, Error::InvalidOption(m) if m.contains("RxC")),
"empty rows: got {err:?}",
);
let err = parse_version_string("4x").unwrap_err();
assert!(
matches!(&err, Error::InvalidOption(m) if m.contains("RxC")),
"empty cols: got {err:?}",
);
let err = parse_version_string("aax2").unwrap_err();
assert!(
matches!(&err, Error::InvalidOption(m) if m.contains("RxC")),
"non-digit rows: got {err:?}"
);
assert!(
matches!(&err, Error::InvalidOption(m) if !m.contains("out of range")),
"non-digit rows must short-circuit before u8 parse; got {err:?}"
);
let err = parse_version_string("4xab").unwrap_err();
assert!(
matches!(&err, Error::InvalidOption(m) if m.contains("RxC")),
"non-digit cols: got {err:?}"
);
assert!(
matches!(&err, Error::InvalidOption(m) if !m.contains("out of range")),
"non-digit cols must short-circuit before u8 parse; got {err:?}"
);
let err = parse_version_string("256x2").unwrap_err();
assert!(
matches!(&err, Error::InvalidOption(m) if m.contains("rows component out of range")),
"rows overflow: got {err:?}",
);
let err = parse_version_string("4x256").unwrap_err();
assert!(
matches!(
&err,
Error::InvalidOption(m) if m.contains("columns component out of range")
),
"cols overflow: got {err:?}",
);
}
#[test]
fn parse_raw_codewords_micropdf_token_format_and_range() {
assert_eq!(parse_raw_codewords(b"").unwrap(), Vec::<u32>::new());
assert_eq!(parse_raw_codewords(b"^000").unwrap(), vec![0]);
assert_eq!(parse_raw_codewords(b"^123").unwrap(), vec![123]);
assert_eq!(
parse_raw_codewords(b"^928").unwrap(),
vec![928],
"928 must fit (kills `> 928` → `>= 928`)"
);
assert_eq!(parse_raw_codewords(b"^123^456").unwrap(), vec![123, 456]);
assert_eq!(parse_raw_codewords(b"^001^002^003").unwrap(), vec![1, 2, 3]);
for (input, want_value) in [(b"^929" as &[u8], "929"), (b"^999", "999")] {
let err = parse_raw_codewords(input).unwrap_err();
assert!(
err.contains("must be 0..=928") && err.contains(want_value),
"{input:?} must pin value-range diagnostic + {want_value:?}; got {err:?}"
);
}
for (input, want_byte) in [(b"$123" as &[u8], "0x24"), (b"1234", "0x31")] {
let err = parse_raw_codewords(input).unwrap_err();
assert!(
err.contains("expected `^` at offset 0") && err.contains(want_byte),
"{input:?} must pin bad-prefix diagnostic + {want_byte:?}; got {err:?}"
);
}
for (input, want_offset) in [(b"^12A" as &[u8], "at offset 3"), (b"^A23", "at offset 1")] {
let err = parse_raw_codewords(input).unwrap_err();
assert!(
err.contains("non-digit") && err.contains(want_offset),
"{input:?} must pin non-digit diagnostic + {want_offset:?}; got {err:?}"
);
}
for (input, want_count) in [
(b"^123^" as &[u8], "1 trailing byte(s)"),
(b"^123^45", "3 trailing byte(s)"),
(b"^", "1 trailing byte(s)"),
(b"^12", "3 trailing byte(s)"),
] {
let err = parse_raw_codewords(input).unwrap_err();
assert!(
err.contains("trailing") && err.contains(want_count),
"{input:?} must pin trailing-bytes diagnostic + {want_count:?}; got {err:?}"
);
}
}
#[test]
fn append_bits_msb_first_bit_emission() {
let mut out = vec![];
append_bits(&mut out, 42, 0);
assert_eq!(out, Vec::<u8>::new(), "n=0 → no-op");
let mut out = vec![];
append_bits(&mut out, 0, 1);
assert_eq!(out, vec![0], "0 in 1 bit → [0]");
let mut out = vec![];
append_bits(&mut out, 1, 1);
assert_eq!(out, vec![1], "1 in 1 bit → [1]");
let mut out = vec![];
append_bits(&mut out, 0b110, 3);
assert_eq!(
out,
vec![1, 1, 0],
"0b110 in 3 bits → [1, 1, 0] MSB-first (LSB mutant would give [0, 1, 1])"
);
let mut out = vec![];
append_bits(&mut out, 0b100, 3);
assert_eq!(
out,
vec![1, 0, 0],
"0b100 in 3 bits → [1, 0, 0] MSB-first (LSB mutant would give [0, 0, 1])"
);
let mut out = vec![];
append_bits(&mut out, 0b1100, 4);
assert_eq!(out, vec![1, 1, 0, 0]);
let mut out = vec![];
append_bits(&mut out, 0b1111_1111, 8);
assert_eq!(out, vec![1, 1, 1, 1, 1, 1, 1, 1], "all-ones 8 bits");
let mut out = vec![];
append_bits(&mut out, 0xAA, 8);
assert_eq!(out, vec![1, 0, 1, 0, 1, 0, 1, 0], "0xAA → alternating 1,0");
let mut out = vec![];
append_bits(&mut out, 0x55, 8);
assert_eq!(out, vec![0, 1, 0, 1, 0, 1, 0, 1], "0x55 → alternating 0,1");
let mut out = vec![5, 9];
append_bits(&mut out, 0b10, 2);
assert_eq!(
out,
vec![5, 9, 1, 0],
"prior [5, 9] preserved, appended [1, 0]"
);
let mut out = vec![];
append_bits(&mut out, 2, 2);
assert_eq!(out, vec![1, 0]);
let mut out = vec![];
append_bits(&mut out, u32::MAX, 32);
assert_eq!(out.len(), 32);
assert!(out.iter().all(|&b| b == 1), "u32::MAX in 32 bits → 32 ones");
for v in 0u32..16 {
let mut out = vec![];
append_bits(&mut out, v, 4);
assert_eq!(out.len(), 4, "v={v}: length must equal n=4");
let reconstructed: u32 = out
.iter()
.enumerate()
.map(|(i, &b)| (b as u32) << (3 - i))
.sum();
assert_eq!(
reconstructed, v,
"v={v} → bits {out:?} must reconstruct via MSB-first base-2"
);
}
}
#[test]
fn select_cca_metric_constrained_respects_row_and_col_constraints() {
let m = select_cca_metric_constrained(5, 2, 0).expect("5 cws c=2");
assert_eq!(m, (2, 5, 4, 39, 0, 19), "no urows → smallest c=2 row");
let m = select_cca_metric_constrained(5, 2, 8).expect("5 cws c=2 r=8");
assert_eq!(
m,
(2, 8, 5, 8, 0, 40),
"urows=8 must skip r=5 row 0 and land on r=8 row 3"
);
let m = select_cca_metric_constrained(5, 3, 5).expect("5 cws c=3 r=5");
assert_eq!(m, (3, 5, 5, 1, 33, 13));
assert_eq!(
select_cca_metric_constrained(12, 3, 5),
None,
"12 cws should not fit c=3 r=5 (cap=10) and no other c=3 r=5 row exists"
);
let m = select_cca_metric_constrained(5, 4, 3).expect("5 cws c=4 r=3");
assert_eq!(m, (4, 3, 4, 40, 20, 52));
let m = select_cca_metric_constrained(20, 4, 7).expect("20 cws c=4 r=7 (cap=20)");
assert_eq!(
m,
(4, 7, 8, 29, 9, 41),
"cap==cw boundary must be accepted (>= not >)"
);
assert_eq!(
select_cca_metric_constrained(21, 4, 7),
None,
"21 cws should exceed c=4 r=7 max capacity (20)"
);
let m = select_cca_metric_constrained(0, 0, 0).expect("0 cws no constraints");
assert_eq!(m, (2, 5, 4, 39, 0, 19), "first row of CCA_METRICS");
for cw_count in [0, 1, 5, 10, 50, 100] {
for ucols in [0u8, 2, 3, 4] {
assert_eq!(
select_cca_metric(cw_count, ucols),
select_cca_metric_constrained(cw_count, ucols, 0),
"select_cca_metric must equal the (.., .., urows=0) variant for cw={cw_count} ucols={ucols}"
);
}
}
}
}