pub(crate) mod clusters;
use crate::encoding::BitMatrix;
use crate::error::Error;
use crate::options::Options;
const ALPHA: usize = 0;
const LOWER: usize = 1;
const MIXED: usize = 2;
const PUNCT: usize = 3;
const AL: i16 = -6; const LL: i16 = -7; const ML: i16 = -8; const PL: i16 = -9; const AS: i16 = -10; const PS: i16 = -11;
const INF: i32 = 40000;
#[rustfmt::skip]
const CHARMAPS: [[i16; 4]; 30] = [
[b'A' as i16, b'a' as i16, b'0' as i16, b';' as i16],
[b'B' as i16, b'b' as i16, b'1' as i16, b'<' as i16],
[b'C' as i16, b'c' as i16, b'2' as i16, b'>' as i16],
[b'D' as i16, b'd' as i16, b'3' as i16, b'@' as i16],
[b'E' as i16, b'e' as i16, b'4' as i16, b'[' as i16],
[b'F' as i16, b'f' as i16, b'5' as i16, b'\\' as i16],
[b'G' as i16, b'g' as i16, b'6' as i16, b']' as i16],
[b'H' as i16, b'h' as i16, b'7' as i16, b'_' as i16],
[b'I' as i16, b'i' as i16, b'8' as i16, b'`' as i16],
[b'J' as i16, b'j' as i16, b'9' as i16, b'~' as i16],
[b'K' as i16, b'k' as i16, b'&' as i16, b'!' as i16],
[b'L' as i16, b'l' as i16, 13, 13],
[b'M' as i16, b'm' as i16, 9, 9],
[b'N' as i16, b'n' as i16, b',' as i16, b',' as i16],
[b'O' as i16, b'o' as i16, b':' as i16, b':' as i16],
[b'P' as i16, b'p' as i16, b'#' as i16, 10],
[b'Q' as i16, b'q' as i16, b'-' as i16, b'-' as i16],
[b'R' as i16, b'r' as i16, b'.' as i16, b'.' as i16],
[b'S' as i16, b's' as i16, b'$' as i16, b'$' as i16],
[b'T' as i16, b't' as i16, b'/' as i16, b'/' as i16],
[b'U' as i16, b'u' as i16, b'+' as i16, b'"' as i16],
[b'V' as i16, b'v' as i16, b'%' as i16, b'|' as i16],
[b'W' as i16, b'w' as i16, b'*' as i16, b'*' as i16],
[b'X' as i16, b'x' as i16, b'=' as i16, b'(' as i16],
[b'Y' as i16, b'y' as i16, b'^' as i16, b')' as i16],
[b'Z' as i16, b'z' as i16, PL, b'?' as i16],
[b' ' as i16, b' ' as i16, b' ' as i16, b'{' as i16],
[LL, AS, LL, b'}' as i16],
[ML, ML, AL, b'\'' as i16],
[PS, PS, PS, AL],
];
#[rustfmt::skip]
const LATLEN: [[i32; 4]; 4] = [
[0, 1, 1, 2], [2, 0, 1, 2], [1, 1, 0, 1], [1, 2, 2, 0], ];
#[rustfmt::skip]
const LATSEQ: [[&[i16]; 4]; 4] = [
[&[], &[LL], &[ML], &[ML, PL]], [&[ML, AL], &[], &[ML], &[ML, PL]], [&[AL], &[LL], &[], &[PL]], [&[AL], &[AL, LL], &[AL, ML], &[]], ];
#[rustfmt::skip]
const SHFTLEN: [[i32; 4]; 4] = [
[INF, INF, INF, 1], [1, INF, INF, 1], [INF, INF, INF, 1], [INF, INF, INF, INF], ];
fn encode_in(submode: usize, ch: i16) -> Option<u8> {
CHARMAPS
.iter()
.position(|row| row[submode] == ch)
.map(|i| i as u8)
}
fn in_submode(submode: usize, ch: i16) -> bool {
encode_in(submode, ch).is_some()
}
pub(crate) fn text_codewords(input: &str) -> Result<Vec<u16>, String> {
let bytes: Vec<i16> = input.bytes().map(|b| b as i16).collect();
for &b in &bytes {
if !(0..=3).any(|s| in_submode(s, b)) {
return Err(format!("PDF417: byte {b:#x} is not in any text sub-mode"));
}
}
let mut cur_seq: [Vec<i16>; 4] = Default::default();
let mut cur_len: [i32; 4] = [INF; 4];
cur_len[ALPHA] = 0;
for &ch in &bytes {
loop {
let mut improved = false;
for x in 0..4 {
for y in 0..4 {
if cur_len[x] >= INF {
continue;
}
let cost = cur_len[x] + LATLEN[x][y];
if cost < cur_len[y] {
cur_len[y] = cost;
let src = cur_seq[x].clone();
cur_seq[y] = src;
cur_seq[y].extend_from_slice(LATSEQ[x][y]);
improved = true;
}
}
}
if !improved {
break;
}
}
let mut nxt_seq: [Vec<i16>; 4] = Default::default();
let mut nxt_len: [i32; 4] = [INF; 4];
for x in 0..4 {
if !in_submode(x, ch) {
continue;
}
let cost = cur_len[x].saturating_add(1);
if cost < nxt_len[x] {
nxt_len[x] = cost;
let mut s = cur_seq[x].clone();
s.push(ch);
nxt_seq[x] = s;
}
for y in 0..4 {
if y == x || SHFTLEN[y][x] >= INF || cur_len[y] >= INF {
continue;
}
let cost = cur_len[y] + SHFTLEN[y][x] + 1;
if cost < nxt_len[y] {
nxt_len[y] = cost;
let mut s = cur_seq[y].clone();
s.push(if x == ALPHA { AS } else { PS });
s.push(ch);
nxt_seq[y] = s;
}
}
}
cur_len = nxt_len;
cur_seq = nxt_seq;
}
let mut min_len = INF;
let mut txtseq: Vec<i16> = Vec::new();
for k in 0..4 {
if cur_len[k] < min_len {
min_len = cur_len[k];
txtseq = cur_seq[k].clone();
}
}
let mut text: Vec<u8> = Vec::new();
let mut submode = ALPHA;
let mut i = 0;
while i < txtseq.len() {
let ch = txtseq[i];
text.push(
encode_in(submode, ch)
.ok_or_else(|| format!("internal: char {ch:#x} not in submode {submode}"))?,
);
i += 1;
if ch == AS || ch == PS {
let shifted = if ch == AS { ALPHA } else { PUNCT };
let next_ch = txtseq[i];
text.push(
encode_in(shifted, next_ch).ok_or_else(|| {
format!("internal: shifted char {next_ch:#x} not in {shifted}")
})?,
);
i += 1;
}
match ch {
AL => submode = ALPHA,
LL => submode = LOWER,
ML => submode = MIXED,
PL => submode = PUNCT,
_ => {}
}
}
if text.len() % 2 != 0 {
text.push(29);
}
Ok(text
.chunks_exact(2)
.map(|p| u16::from(p[0]) * 30 + u16::from(p[1]))
.collect())
}
pub(crate) fn byte_codewords(bytes: &[u8]) -> Vec<u16> {
let mut out = Vec::with_capacity((bytes.len() / 6) * 5 + bytes.len() % 6);
let full_groups = bytes.len() / 6;
for k in 0..full_groups {
let off = k * 6;
let mut v: u64 = 0;
for &b in &bytes[off..off + 6] {
v = (v << 8) | b as u64;
}
let cw0 = v / 656_100_000_000;
let r = v % 656_100_000_000;
let cw1 = r / 729_000_000;
let r = r % 729_000_000;
let cw2 = r / 810_000;
let r = r % 810_000;
let cw3 = r / 900;
let cw4 = r % 900;
out.push(cw0 as u16);
out.push(cw1 as u16);
out.push(cw2 as u16);
out.push(cw3 as u16);
out.push(cw4 as u16);
}
for &b in &bytes[full_groups * 6..] {
out.push(b as u16);
}
out
}
pub(crate) fn numeric_codewords(digits: &str) -> Result<Vec<u16>, String> {
if !digits.bytes().all(|b| b.is_ascii_digit()) {
return Err("PDF417 numeric: input must contain only ASCII digits".into());
}
let mut out = Vec::new();
let bytes = digits.as_bytes();
let mut i = 0;
while i < bytes.len() {
let end = (i + 44).min(bytes.len());
let mut gmod: Vec<u32> = Vec::with_capacity(end - i + 1);
gmod.push(1);
for &b in &bytes[i..end] {
gmod.push((b - b'0') as u32);
}
let mut cwn: Vec<u16> = Vec::new();
while !gmod.is_empty() {
let mut new_gmod = Vec::new();
let mut val: u32 = 0;
let mut started = false;
for &d in &gmod {
val = val * 10 + d;
if val >= 900 {
started = true;
new_gmod.push(val / 900);
val %= 900;
} else if started {
new_gmod.push(0);
}
}
cwn.push(val as u16);
gmod = new_gmod;
}
cwn.reverse();
out.extend(cwn);
i = end;
}
Ok(out)
}
const LATCH_TEXT: u16 = 900;
const LATCH_BYTE: u16 = 901;
const LATCH_NUMERIC: u16 = 902;
const LATCH_BYTE_SHIFT: u16 = 913; const LATCH_BYTE_6: u16 = 924;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Mode {
Text,
Numeric,
Byte,
}
fn is_text_byte(b: u8) -> bool {
let v = b as i16;
CHARMAPS.iter().any(|row| row.contains(&v))
}
pub(crate) fn data_codewords(msg: &[u8]) -> Result<Vec<u16>, String> {
let n = msg.len();
if n == 0 {
return Ok(Vec::new());
}
let mut numdigits = vec![0usize; n + 1];
let mut numtext = vec![0usize; n + 1];
let mut numbytes = vec![0usize; n + 1];
for i in (0..n).rev() {
if msg[i].is_ascii_digit() {
numdigits[i] = numdigits[i + 1] + 1;
}
if is_text_byte(msg[i]) && numdigits[i] < 13 {
numtext[i] = numtext[i + 1] + 1;
}
if numtext[i] < 5 && numdigits[i] < 13 {
numbytes[i] = numbytes[i + 1] + 1;
}
}
let mut out: Vec<u16> = Vec::new();
let mut state = Mode::Text;
let mut p = 0;
while p < n {
let nd = numdigits[p];
if nd >= 13 || (nd == n - p && nd >= 8) {
out.push(LATCH_NUMERIC);
let segment = std::str::from_utf8(&msg[p..p + nd])
.map_err(|_| "PDF417: numeric segment contained non-ASCII bytes".to_string())?;
out.extend(numeric_codewords(segment)?);
state = Mode::Numeric;
p += nd;
continue;
}
let nt = numtext[p];
if nt >= 5 {
if state != Mode::Text {
out.push(LATCH_TEXT);
}
let segment = std::str::from_utf8(&msg[p..p + nt])
.map_err(|_| "PDF417: text segment contained non-ASCII bytes".to_string())?;
out.extend(text_codewords(segment)?);
state = Mode::Text;
p += nt;
continue;
}
let nb = numbytes[p];
if nb == 1 && state == Mode::Text {
out.push(LATCH_BYTE_SHIFT);
out.push(msg[p] as u16);
p += 1;
continue;
}
if nb % 6 == 0 {
out.push(LATCH_BYTE_6);
} else {
out.push(LATCH_BYTE);
}
out.extend(byte_codewords(&msg[p..p + nb]));
state = Mode::Byte;
p += nb;
}
Ok(out)
}
pub fn pdf417_cws(input: &[u8], eclevel: u8, columns: usize) -> Result<Vec<u16>, String> {
if eclevel > 8 {
return Err(format!("PDF417: eclevel must be 0..=8, got {eclevel}"));
}
let datcws = data_codewords(input)?;
let m = datcws.len();
if m > 926 {
return Err(format!("PDF417: data too long ({m} codewords > 926)"));
}
let k = 1usize << (eclevel as usize + 1);
let c = if columns == 0 {
(((m + k) as f64 / 3.0).sqrt().round() as usize).max(1)
} else {
columns
};
let mut r = (m + k + 1).div_ceil(c).max(3);
if r > 90 {
return Err("PDF417: insufficient capacity (r > 90)".into());
}
let n = c * r - k;
if n > 928 {
return Err(format!("PDF417: n {n} > 928"));
}
if c * r < m + k + 1 {
r = (m + k + 1).div_ceil(c);
}
let total = c * r;
let mut cws: Vec<u16> = vec![0; total];
cws[0] = n as u16;
cws[1..=m].copy_from_slice(&datcws);
for slot in cws.iter_mut().take(n).skip(m + 1) {
*slot = 900;
}
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws[n..n + k].copy_from_slice(&check);
Ok(cws)
}
#[rustfmt::skip]
const START_PATTERN: [u8; 17] = [
1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0,
];
#[rustfmt::skip]
const STOP_PATTERN: [u8; 18] = [
1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1,
];
fn cw_to_bits(cw: u16, cluster: usize) -> [u8; 17] {
let v = clusters::ALL_CLUSTERS[cluster][cw as usize];
let mut out = [0u8; 17];
for (i, slot) in out.iter_mut().enumerate() {
*slot = ((v >> (16 - i)) & 1) as u8;
}
out
}
pub fn pdf417_render(input: &[u8], eclevel: u8, columns: usize) -> Result<BitMatrix, String> {
render_inner(input, eclevel, columns, false)
}
pub fn pdf417_render_truncated(
input: &[u8],
eclevel: u8,
columns: usize,
) -> Result<BitMatrix, String> {
render_inner(input, eclevel, columns, true)
}
pub(crate) fn pdf417_render_ccc(
bytes: &[u8],
eclevel: u8,
columns: usize,
) -> Result<BitMatrix, String> {
if eclevel > 8 {
return Err(format!("PDF417 CC-C: eclevel must be 0..=8, got {eclevel}"));
}
if columns == 0 {
return Err("PDF417 CC-C: explicit columns required (auto-size not supported)".into());
}
let datcws = crate::symbology::micropdf417::pack_ccb_datcws(bytes);
let m = datcws.len();
let k = 1usize << (eclevel as usize + 1);
let mut r = (m + k + 1).div_ceil(columns).max(3);
if r > 90 {
return Err(format!(
"PDF417 CC-C: insufficient capacity ({m} datcws + {k} ecc > {} slots)",
90 * columns,
));
}
if columns * r < m + k + 1 {
r = (m + k + 1).div_ceil(columns);
}
let total = columns * r;
let n = total - k;
if n > 928 {
return Err(format!(
"PDF417 CC-C: insufficient capacity ({n} data codewords exceeds the 928 PDF417 maximum)"
));
}
let mut cws: Vec<u16> = vec![0; total];
cws[0] = n as u16;
cws[1..=m].copy_from_slice(&datcws);
for slot in cws.iter_mut().take(n).skip(m + 1) {
*slot = 900;
}
let check = crate::util::rs_gf929::encode(&cws[..n], k);
cws[n..n + k].copy_from_slice(&check);
render_cws_inner(&cws, eclevel, columns, r, false)
}
fn render_inner(
input: &[u8],
eclevel: u8,
columns: usize,
truncated: bool,
) -> Result<BitMatrix, String> {
let cws = pdf417_cws(input, eclevel, columns)?;
let n = cws[0] as usize;
let k = 1usize << (eclevel as usize + 1);
let total = cws.len();
debug_assert_eq!(n + k, total);
let c = if columns == 0 {
(((n + k - 1).max(1) as f64 / 3.0).sqrt().round() as usize).max(1)
} else {
columns
};
let r = total / c;
render_cws_inner(&cws, eclevel, c, r, truncated)
}
fn render_cws_inner(
cws: &[u16],
eclevel: u8,
c: usize,
r: usize,
truncated: bool,
) -> Result<BitMatrix, String> {
let rwid = if truncated {
17 * (c + 2) + 1
} else {
17 * (c + 3) + 18
};
let mut bm = BitMatrix::new(rwid, r);
for i in 0..r {
let cluster = i % 3;
let lcw = match i % 3 {
0 => (i / 3) * 30 + (r - 1) / 3,
1 => (i / 3) * 30 + eclevel as usize * 3 + (r - 1) % 3,
_ => (i / 3) * 30 + c - 1,
} as u16;
let mut row: Vec<u8> = Vec::with_capacity(rwid);
row.extend_from_slice(&START_PATTERN);
row.extend_from_slice(&cw_to_bits(lcw, cluster));
for j in 0..c {
row.extend_from_slice(&cw_to_bits(cws[c * i + j], cluster));
}
if truncated {
row.push(1);
} else {
let rcw = match i % 3 {
0 => (i / 3) * 30 + c - 1,
1 => (i / 3) * 30 + (r - 1) / 3,
_ => (i / 3) * 30 + eclevel as usize * 3 + (r - 1) % 3,
} as u16;
row.extend_from_slice(&cw_to_bits(rcw, cluster));
row.extend_from_slice(&STOP_PATTERN);
}
debug_assert_eq!(row.len(), rwid);
for (x, &bit) in row.iter().enumerate() {
if bit != 0 {
bm.set(x, i, true);
}
}
}
Ok(bm)
}
pub fn encode(data: &str, opts: &Options) -> Result<BitMatrix, Error> {
let eclevel: u8 = match opts.get("eclevel") {
Some(s) => s
.parse::<u8>()
.map_err(|_| Error::InvalidOption(format!("eclevel={s} (want 0..=8)")))?,
None => 2,
};
if eclevel > 8 {
return Err(Error::InvalidOption(format!(
"eclevel={eclevel} out of range 0..=8"
)));
}
let columns: usize = match opts.get("columns") {
Some(s) => s
.parse::<usize>()
.map_err(|_| Error::InvalidOption(format!("columns={s} (want 0..=30)")))?,
None => 0,
};
if columns > 30 {
return Err(Error::InvalidOption(format!(
"columns={columns} out of range 0..=30"
)));
}
pdf417_render(data.as_bytes(), eclevel, columns).map_err(Error::InvalidData)
}
pub fn encode_truncated(data: &str, opts: &Options) -> Result<BitMatrix, Error> {
let eclevel: u8 = match opts.get("eclevel") {
Some(s) => s
.parse::<u8>()
.map_err(|_| Error::InvalidOption(format!("eclevel={s} (want 0..=8)")))?,
None => 2,
};
if eclevel > 8 {
return Err(Error::InvalidOption(format!(
"eclevel={eclevel} out of range 0..=8"
)));
}
let columns: usize = match opts.get("columns") {
Some(s) => s
.parse::<usize>()
.map_err(|_| Error::InvalidOption(format!("columns={s} (want 0..=30)")))?,
None => 0,
};
if columns > 30 {
return Err(Error::InvalidOption(format!(
"columns={columns} out of range 0..=30"
)));
}
pdf417_render_truncated(data.as_bytes(), eclevel, columns).map_err(Error::InvalidData)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ccc_over_capacity_returns_err_not_panic() {
let big = vec![b'A'; 2000];
let r = pdf417_render_ccc(&big, 0, 30);
assert!(
r.is_err(),
"over-capacity CC-C must return Err, not panic / not Ok"
);
}
#[test]
fn text_pdf417_canonical() {
let cws = text_codewords("PDF417")
.expect("text_codewords(\"PDF417\") (canonical mixed-case PDF417 word) must succeed");
assert_eq!(cws, vec![453, 178, 121, 239]);
}
#[test]
fn text_uppercase_only() {
let cws = text_codewords("ABCD").expect(
"text_codewords(\"ABCD\") (pure-uppercase staying in alpha sub-mode, no latches) must succeed",
);
assert_eq!(cws, vec![1, 63]);
}
#[test]
fn text_padding_for_odd_length() {
let cws = text_codewords("ABCDE").expect(
"text_codewords(\"ABCDE\") (5-char odd-length → 5 sub-codewords + 1 pad(29) → 3 pairs) must succeed",
);
assert_eq!(cws, vec![1, 63, 149]);
}
#[test]
fn text_hello_world_matches_bwip_js() {
let cws = text_codewords("Hello World").expect(
"text_codewords(\"Hello World\") (lower-latch LL + punct-shift + alpha-shift on embedded capital) must succeed",
);
assert_eq!(cws, vec![237, 131, 344, 807, 674, 521, 119]);
}
#[test]
fn numeric_short_run() {
let cws = numeric_codewords("1234567890123").expect(
"numeric_codewords(\"1234567890123\") (13-digit numeric-mode segment after 902 latch → 5 codewords) must succeed",
);
assert_eq!(cws, vec![17, 110, 836, 811, 223]);
}
#[test]
fn numeric_chunks_at_44_digits() {
let cws = numeric_codewords("123456789012345678901234567890123456789012345").expect(
"numeric_codewords(45-digit input) (44-digit chunk → 15 cws + 1-digit tail chunk → 1 cw = 16 total) must succeed",
);
assert_eq!(
cws,
vec![491, 81, 137, 450, 302, 67, 15, 174, 492, 862, 667, 475, 869, 12, 434, 15]
);
}
#[test]
fn numeric_rejects_non_digits() {
let err = numeric_codewords("123ABC").unwrap_err();
assert!(
err.contains("PDF417 numeric:"),
"diagnostic must carry the symbology prefix; got {err}"
);
assert!(
err.contains("input must contain"),
"diagnostic must carry the predicate verb; got {err}"
);
assert!(
err.contains("only ASCII digits"),
"diagnostic must carry the narrowing object; got {err}"
);
}
#[test]
fn byte_full_group() {
let cws = byte_codewords(&[1, 2, 3, 4, 5, 6]);
assert_eq!(cws, vec![1, 620, 89, 74, 846]);
}
#[test]
fn byte_two_full_groups() {
let cws = byte_codewords(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
assert_eq!(cws, vec![1, 620, 89, 74, 846, 11, 705, 58, 895, 432]);
}
#[test]
fn byte_group_plus_tail() {
let cws = byte_codewords(&[1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(cws, vec![1, 620, 89, 74, 846, 7, 8]);
}
#[test]
fn byte_short_run() {
let cws = byte_codewords(b"ABCD");
assert_eq!(cws, vec![65, 66, 67, 68]);
}
#[test]
fn data_codewords_match_bwip_js() {
let cases: &[(&[u8], &[u16])] = &[
(b"PDF417", &[453, 178, 121, 239]),
(b"Hello World", &[237, 131, 344, 807, 674, 521, 119]),
(b"ABCD", &[901, 65, 66, 67, 68]),
(b"12345", &[841, 63, 125]),
(b"1234567890123", &[902, 17, 110, 836, 811, 223]),
(b"PDF417 12345", &[453, 178, 121, 236, 32, 94, 179]),
(b"AB\xC3\xA9CD", &[924, 109, 329, 327, 474, 300]),
(b"12345678", &[902, 138, 628, 478]),
];
for &(input, want) in cases {
let got = data_codewords(input).unwrap_or_else(|e| {
panic!(
"data_codewords({:?}) (PDF417 mode-classifier corpus item) must succeed; got Err: {e}",
std::str::from_utf8(input).unwrap_or("<non-utf8>")
)
});
assert_eq!(
got,
want,
"data_codewords mismatch for {:?}",
std::str::from_utf8(input).unwrap()
);
}
}
#[test]
fn pdf417_cws_matches_bwip_js() {
let cws = pdf417_cws(b"PDF417", 2, 0).expect(
"pdf417_cws(b\"PDF417\", eclevel=2) (c=2 r=7 k=8 n=6 total=14, mixed-case word) must succeed",
);
assert_eq!(
cws,
vec![6, 453, 178, 121, 239, 900, 466, 823, 655, 326, 814, 259, 528, 611]
);
let cws = pdf417_cws(b"Hello World", 1, 0).expect(
"pdf417_cws(b\"Hello World\", eclevel=1) (c=2 r=6 k=4 n=8 total=12, mixed-case + space) must succeed",
);
assert_eq!(
cws,
vec![8, 237, 131, 344, 807, 674, 521, 119, 661, 754, 265, 571]
);
let cws = pdf417_cws(b"ABCD", 0, 0).expect(
"pdf417_cws(b\"ABCD\", eclevel=0) (c=2 r=4 k=2 m=5 n=6 total=8, no 900-padding because n-m-1=0) must succeed",
);
assert_eq!(cws, vec![6, 901, 65, 66, 67, 68, 911, 504]);
let cws = pdf417_cws(b"1234567890123", 1, 0).expect(
"pdf417_cws(b\"1234567890123\", eclevel=1) (c=2 r=6 k=4 n=8 total=12, 13-digit numeric via 902 latch) must succeed",
);
assert_eq!(
cws,
vec![8, 902, 17, 110, 836, 811, 223, 900, 234, 914, 111, 325]
);
}
#[test]
fn pdf417_render_matches_bwip_js_pixs() {
let want_pixs = [
1u8, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1,
1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1,
1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0,
0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0,
1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0,
1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0,
0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1,
0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0,
0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0,
1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0,
1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0,
1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1,
1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1,
0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1,
1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1,
1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0,
0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1,
0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1,
];
let bm = pdf417_render(b"PDF417", 2, 0).expect(
"pdf417_render(b\"PDF417\", eclevel=2) (r=7 c=2 rwid=103 total pixs=721, canonical golden) must succeed",
);
assert_eq!(bm.width(), 103);
assert_eq!(bm.height(), 7);
let mut got_pixs: Vec<u8> = Vec::with_capacity(bm.width() * bm.height());
for y in 0..bm.height() {
for x in 0..bm.width() {
got_pixs.push(bm.get(x, y) as u8);
}
}
assert_eq!(got_pixs, want_pixs);
}
#[test]
fn pdf417_render_truncated_matches_bwip_js_pixs() {
#[rustfmt::skip]
let want_pixs: [u8; 483] = [
1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0,
0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1,
0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1,
1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0,
1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1,
1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0,
0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,
1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1,
0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1,
0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1,
1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0,
1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1,
0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0,
0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0,
0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0,
0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0,
0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1,
1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1,
0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1,
1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0,
1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1,
1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1,
1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0,
1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0,
0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0,
0, 0, 1,
];
let bm = pdf417_render_truncated(b"PDF417", 2, 0).expect(
"pdf417_render_truncated(b\"PDF417\", eclevel=2) (truncated: r=7 c=2 rwid=69 — full=103 minus 34-module RAP+right-side) must succeed",
);
assert_eq!(bm.width(), 69);
assert_eq!(bm.height(), 7);
let mut got_pixs: Vec<u8> = Vec::with_capacity(bm.width() * bm.height());
for y in 0..bm.height() {
for x in 0..bm.width() {
got_pixs.push(bm.get(x, y) as u8);
}
}
assert_eq!(got_pixs, want_pixs);
}
#[test]
fn encode_pdf417_option_parsing_rejection_arms() {
let opts = Options::default().with("eclevel", "abc");
let res = encode("hi", &opts);
match res {
Err(Error::InvalidOption(msg)) => assert!(
msg.contains("eclevel=abc") && msg.contains("(want 0..=8)"),
"expected eclevel parse-fail diagnostic, got: {msg:?}"
),
other => panic!("expected InvalidOption(eclevel-parse), got {other:?}"),
}
let opts = Options::default().with("eclevel", "9");
let res = encode("hi", &opts);
match res {
Err(Error::InvalidOption(msg)) => assert!(
msg.contains("eclevel=9") && msg.contains("out of range"),
"expected eclevel range diagnostic, got: {msg:?}"
),
other => panic!("expected InvalidOption(eclevel-range), got {other:?}"),
}
let opts = Options::default().with("columns", "xyz");
let res = encode("hi", &opts);
match res {
Err(Error::InvalidOption(msg)) => assert!(
msg.contains("columns=xyz") && msg.contains("(want 0..=30)"),
"expected columns parse-fail diagnostic, got: {msg:?}"
),
other => panic!("expected InvalidOption(columns-parse), got {other:?}"),
}
let opts = Options::default().with("columns", "31");
let res = encode("hi", &opts);
match res {
Err(Error::InvalidOption(msg)) => assert!(
msg.contains("columns=31") && msg.contains("out of range"),
"expected columns range diagnostic, got: {msg:?}"
),
other => panic!("expected InvalidOption(columns-range), got {other:?}"),
}
let opts = Options::default()
.with("eclevel", "8")
.with("columns", "30");
assert!(
encode("hi", &opts).is_ok(),
"boundary eclevel=8/columns=30 must succeed (kills `> 7`/`> 29` mutants)"
);
}
#[test]
fn encode_truncated_pdf417_option_parsing_rejection_arms() {
let opts = Options::default().with("eclevel", "abc");
match encode_truncated("hi", &opts) {
Err(Error::InvalidOption(msg)) => assert!(
msg.contains("eclevel=abc") && msg.contains("(want 0..=8)"),
"expected eclevel parse-fail diagnostic, got: {msg:?}"
),
other => panic!("expected InvalidOption(eclevel-parse), got {other:?}"),
}
let opts = Options::default().with("eclevel", "9");
match encode_truncated("hi", &opts) {
Err(Error::InvalidOption(msg)) => assert!(
msg.contains("eclevel=9") && msg.contains("out of range"),
"expected eclevel range diagnostic, got: {msg:?}"
),
other => panic!("expected InvalidOption(eclevel-range), got {other:?}"),
}
let opts = Options::default().with("columns", "xyz");
match encode_truncated("hi", &opts) {
Err(Error::InvalidOption(msg)) => assert!(
msg.contains("columns=xyz") && msg.contains("(want 0..=30)"),
"expected columns parse-fail diagnostic, got: {msg:?}"
),
other => panic!("expected InvalidOption(columns-parse), got {other:?}"),
}
let opts = Options::default().with("columns", "31");
match encode_truncated("hi", &opts) {
Err(Error::InvalidOption(msg)) => assert!(
msg.contains("columns=31") && msg.contains("out of range"),
"expected columns range diagnostic, got: {msg:?}"
),
other => panic!("expected InvalidOption(columns-range), got {other:?}"),
}
let opts = Options::default()
.with("eclevel", "8")
.with("columns", "30");
assert!(
encode_truncated("hi", &opts).is_ok(),
"boundary eclevel=8/columns=30 must succeed (kills `> 7`/`> 29` mutants in truncated path)"
);
}
#[test]
fn encode_in_per_submode_lookups() {
assert!(in_submode(0, b'A' as i16));
assert!(!in_submode(0, b'a' as i16));
assert!(in_submode(1, b'a' as i16));
assert!(!in_submode(1, b'A' as i16));
assert!(in_submode(2, b'0' as i16));
assert!(is_text_byte(b'A'));
assert!(is_text_byte(b'a'));
assert!(is_text_byte(b'0'));
assert!(!is_text_byte(0xFF));
let in_0 = encode_in(0, b'A' as i16);
let in_1 = encode_in(1, b'A' as i16);
assert!(in_0.is_some());
assert!(in_1.is_none());
}
#[test]
fn is_text_byte_full_charmaps_coverage() {
assert!(
is_text_byte(13),
"CR (13) must be a text byte (CHARMAPS[11] columns 2 and 3)"
);
assert!(
is_text_byte(9),
"TAB (9) must be a text byte (CHARMAPS[12] columns 2 and 3)"
);
assert!(
is_text_byte(10),
"LF (10) must be a text byte (CHARMAPS[15] column 3)"
);
assert!(!is_text_byte(0), "NUL (0) is not in any CHARMAPS column");
assert!(!is_text_byte(8), "BS (8) is not in any CHARMAPS column");
assert!(!is_text_byte(11), "VT (11) is not in any CHARMAPS column");
assert!(!is_text_byte(12), "FF (12) is not in any CHARMAPS column");
assert!(!is_text_byte(14), "SO (14) is not in any CHARMAPS column");
assert!(
!is_text_byte(31),
"US (31) — just before space — is not in any CHARMAPS column"
);
assert!(
!is_text_byte(127),
"DEL (127) is not in any CHARMAPS column"
);
assert!(
!is_text_byte(128),
"0x80 (128, first high byte) is not in any CHARMAPS column"
);
assert!(
!is_text_byte(255),
"0xFF (255) is not in any CHARMAPS column"
);
assert!(is_text_byte(b' '), "SPACE (32) is in CHARMAPS[26]");
assert!(is_text_byte(b'~'), "tilde (126) is in CHARMAPS[9][3]");
assert!(is_text_byte(b'Z'), "alpha last");
assert!(is_text_byte(b'z'), "lower last");
assert!(is_text_byte(b'9'), "digit last");
assert!(is_text_byte(b'!'), "CHARMAPS[10][3]");
assert!(is_text_byte(b'@'), "CHARMAPS[3][3]");
assert!(is_text_byte(b'{'), "CHARMAPS[26][3]");
assert!(is_text_byte(b'}'), "CHARMAPS[27][3]");
assert!(is_text_byte(b'\''), "CHARMAPS[28][3]");
for b in 32u8..=126 {
assert!(
is_text_byte(b),
"all printable ASCII (32..=126) must be text bytes; failed for byte {b} ({:?})",
b as char
);
}
}
#[test]
fn numeric_codewords_short_inputs() {
assert_eq!(numeric_codewords("").unwrap(), Vec::<u16>::new());
let cws = numeric_codewords("5").unwrap();
assert!(!cws.is_empty(), "single digit must produce non-empty cws");
let cws = numeric_codewords("12345").unwrap();
assert!(
!cws.is_empty(),
"numeric_codewords(\"12345\") (5-digit single-chunk numeric mode) must produce non-empty codewords; got len={}",
cws.len()
);
let a = numeric_codewords("12345678").unwrap();
let b = numeric_codewords("12345678").unwrap();
assert_eq!(a, b);
let a = numeric_codewords("12345678").unwrap();
let b = numeric_codewords("87654321").unwrap();
assert_ne!(a, b);
let err = numeric_codewords("12a45").unwrap_err();
assert!(
err.contains("PDF417 numeric:"),
"diagnostic must carry the symbology prefix; got {err}"
);
assert!(
err.contains("input must contain"),
"diagnostic must carry the predicate verb; got {err}"
);
assert!(
err.contains("only ASCII digits"),
"diagnostic must carry the narrowing object; got {err}"
);
}
#[test]
fn cw_to_bits_extracts_msb_first() {
let bits = cw_to_bits(0, 0);
assert_eq!(
bits,
[1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0],
"cw=0 cluster=0 must MSB-first decode 120256"
);
let bits = cw_to_bits(1, 0);
assert_eq!(
bits,
[1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0],
"cw=1 cluster=0 must MSB-first decode 125680"
);
for &(cw, cluster, expected) in &[(0u16, 0usize, 120256u32), (1, 0, 125680), (9, 0, 86080)]
{
let bits = cw_to_bits(cw, cluster);
let v: u32 = bits
.iter()
.enumerate()
.map(|(i, &b)| u32::from(b) * (1u32 << (16 - i)))
.sum();
assert_eq!(
v, expected,
"round-trip: cw={cw} cluster={cluster} bits sum back to {expected}"
);
}
}
}