mod auspost;
mod imb;
mod japanpost;
mod mailmark;
mod postnet;
mod rm4scc;
mod rs;
use crate::error::{Error, Result};
use crate::output::{BitMatrix, Encoding};
use crate::segment::{Mode, Segment};
use crate::symbol::{Symbol, SymbolMeta};
use crate::symbology::Symbology;
use crate::traits::{Decode, Encode};
const ROWS: usize = 3;
const QUIET: usize = 0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BarState {
Tracker,
Ascender,
Descender,
Full,
}
impl BarState {
fn from_parts(ascender: bool, descender: bool) -> BarState {
match (ascender, descender) {
(true, true) => BarState::Full,
(true, false) => BarState::Ascender,
(false, true) => BarState::Descender,
(false, false) => BarState::Tracker,
}
}
fn has_ascender(self) -> bool {
matches!(self, BarState::Ascender | BarState::Full)
}
fn has_descender(self) -> bool {
matches!(self, BarState::Descender | BarState::Full)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PostalVariant {
Postnet,
Planet,
IntelligentMail,
RoyalMail,
KixCode,
AustraliaPost,
JapanPost,
Mailmark,
}
impl PostalVariant {
fn symbology(self) -> Symbology {
match self {
PostalVariant::Postnet => Symbology::Postnet,
PostalVariant::Planet => Symbology::Planet,
PostalVariant::IntelligentMail => Symbology::IntelligentMail,
PostalVariant::RoyalMail => Symbology::RoyalMail,
PostalVariant::KixCode => Symbology::KixCode,
PostalVariant::AustraliaPost => Symbology::AustraliaPost,
PostalVariant::JapanPost => Symbology::JapanPost,
PostalVariant::Mailmark => Symbology::Mailmark,
}
}
fn from_symbology(s: Symbology) -> Option<PostalVariant> {
Some(match s {
Symbology::Postnet => PostalVariant::Postnet,
Symbology::Planet => PostalVariant::Planet,
Symbology::IntelligentMail => PostalVariant::IntelligentMail,
Symbology::RoyalMail => PostalVariant::RoyalMail,
Symbology::KixCode => PostalVariant::KixCode,
Symbology::AustraliaPost => PostalVariant::AustraliaPost,
Symbology::JapanPost => PostalVariant::JapanPost,
Symbology::Mailmark => PostalVariant::Mailmark,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PostalMeta {
pub variant: PostalVariant,
}
impl PostalMeta {
pub fn new(variant: PostalVariant) -> Self {
PostalMeta { variant }
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct PostalEncoder;
impl PostalEncoder {
pub fn new() -> Self {
PostalEncoder
}
pub fn build_postnet(&self, digits: &str) -> Result<Symbol> {
postnet::validate_digits(digits.as_bytes())?;
Ok(numeric_symbol(
PostalVariant::Postnet,
digits.as_bytes().to_vec(),
))
}
pub fn build_planet(&self, digits: &str) -> Result<Symbol> {
postnet::validate_digits(digits.as_bytes())?;
Ok(numeric_symbol(
PostalVariant::Planet,
digits.as_bytes().to_vec(),
))
}
pub fn build_imb(&self, tracking: &str, routing: &str) -> Result<Symbol> {
imb::validate(tracking.as_bytes(), routing.as_bytes())?;
Ok(Symbol::new(
Symbology::IntelligentMail,
vec![
Segment::numeric(tracking.as_bytes().to_vec()),
Segment::numeric(routing.as_bytes().to_vec()),
],
SymbolMeta::Postal(PostalMeta::new(PostalVariant::IntelligentMail)),
))
}
pub fn build_rm4scc(&self, data: &str) -> Result<Symbol> {
rm4scc::validate(data.as_bytes())?;
Ok(alnum_symbol(
PostalVariant::RoyalMail,
data.as_bytes().to_vec(),
))
}
pub fn build_kix(&self, data: &str) -> Result<Symbol> {
rm4scc::validate(data.as_bytes())?;
Ok(alnum_symbol(
PostalVariant::KixCode,
data.as_bytes().to_vec(),
))
}
pub fn build_auspost(&self, dpid: &str, custinfo: &str) -> Result<Symbol> {
if dpid.len() > 8 || !dpid.bytes().all(|b| b.is_ascii_digit()) {
return Err(Error::invalid_data(
"Australia Post DPID must be up to 8 digits",
));
}
let mut padded = vec![b'0'; 8 - dpid.len()];
padded.extend_from_slice(dpid.as_bytes());
auspost::encode(&padded, custinfo.as_bytes())?;
let mut segments = vec![Segment::numeric(padded)];
if !custinfo.is_empty() {
let seg = if custinfo.bytes().all(|b| b.is_ascii_digit()) {
Segment::numeric(custinfo.as_bytes().to_vec())
} else {
Segment::byte(custinfo.as_bytes().to_vec())
};
segments.push(seg);
}
Ok(Symbol::new(
Symbology::AustraliaPost,
segments,
SymbolMeta::Postal(PostalMeta::new(PostalVariant::AustraliaPost)),
))
}
pub fn build_japanpost(&self, data: &str) -> Result<Symbol> {
japanpost::validate(data.as_bytes())?;
Ok(alnum_symbol(
PostalVariant::JapanPost,
data.as_bytes().to_vec(),
))
}
pub fn build_mailmark(&self, data: &str) -> Result<Symbol> {
let canonical = mailmark::canonical(data.as_bytes())?;
mailmark::encode(&canonical)?;
Ok(alnum_symbol(PostalVariant::Mailmark, canonical))
}
}
impl Encode for PostalEncoder {
fn encode(&self, symbol: &Symbol) -> Result<Encoding> {
let variant =
PostalVariant::from_symbology(symbol.symbology).ok_or(Error::Unsupported {
what: "non-postal symbology passed to PostalEncoder",
})?;
if let SymbolMeta::Postal(meta) = &symbol.meta
&& meta.variant != variant
{
return Err(Error::invalid_parameter(
"PostalMeta variant disagrees with symbol symbology",
));
}
let bars = match variant {
PostalVariant::Postnet => postnet::encode(false, &numeric_data(symbol)?)?,
PostalVariant::Planet => postnet::encode(true, &numeric_data(symbol)?)?,
PostalVariant::IntelligentMail => {
let (tracking, routing) = imb_fields(symbol)?;
imb::encode(&tracking, &routing)?
}
PostalVariant::RoyalMail => rm4scc::encode(false, &alnum_data(symbol)?)?,
PostalVariant::KixCode => rm4scc::encode(true, &alnum_data(symbol)?)?,
PostalVariant::AustraliaPost => {
let (dpid, custinfo) = auspost::fields(&symbol.segments)?;
auspost::encode(&dpid, &custinfo)?
}
PostalVariant::JapanPost => japanpost::encode(&alnum_data(symbol)?)?,
PostalVariant::Mailmark => mailmark::encode(&alnum_data(symbol)?)?,
};
Ok(Encoding::Matrix(bars_to_matrix(&bars)))
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct PostalDecoder;
impl PostalDecoder {
pub fn new() -> Self {
PostalDecoder
}
}
impl Decode for PostalDecoder {
fn decode(&self, encoding: &Encoding) -> Result<Symbol> {
let matrix = match encoding {
Encoding::Matrix(m) => m,
Encoding::Linear(_) => {
return Err(Error::Unsupported {
what: "postal decode of a linear pattern",
});
}
};
let bars = matrix_to_bars(matrix)?;
let n = bars.len();
let four_state = bars
.iter()
.any(|b| matches!(b, BarState::Descender | BarState::Full));
if n == 65 {
let (variant, segments) = imb::decode(&bars)?;
return Ok(build_symbol(variant, segments));
}
if four_state {
if matches!(n, 37 | 52 | 67)
&& let Ok((variant, segments)) = auspost::decode(&bars)
{
return Ok(build_symbol(variant, segments));
}
if n == 67
&& let Ok((variant, data)) = japanpost::decode(&bars)
{
return Ok(build_symbol(variant, vec![Segment::alphanumeric(data)]));
}
if matches!(n, 66 | 78)
&& let Ok((variant, data)) = mailmark::decode(&bars)
{
return Ok(build_symbol(variant, vec![Segment::alphanumeric(data)]));
}
}
if !four_state && n >= 7 && (n - 2) % 5 == 0 {
let (variant, digits) = postnet::decode(&bars)?;
return Ok(build_symbol(variant, vec![Segment::numeric(digits)]));
}
if four_state && n % 4 == 2 && n >= 6 {
let (variant, chars) = rm4scc::decode_rm4scc(&bars)?;
return Ok(build_symbol(variant, vec![Segment::alphanumeric(chars)]));
}
if four_state && n % 4 == 0 && n >= 4 {
let (variant, chars) = rm4scc::decode_kix(&bars)?;
return Ok(build_symbol(variant, vec![Segment::alphanumeric(chars)]));
}
Err(Error::undecodable("unrecognized postal bar layout"))
}
}
fn numeric_symbol(variant: PostalVariant, digits: Vec<u8>) -> Symbol {
Symbol::new(
variant.symbology(),
vec![Segment::numeric(digits)],
SymbolMeta::Postal(PostalMeta::new(variant)),
)
}
fn alnum_symbol(variant: PostalVariant, data: Vec<u8>) -> Symbol {
Symbol::new(
variant.symbology(),
vec![Segment::alphanumeric(data)],
SymbolMeta::Postal(PostalMeta::new(variant)),
)
}
fn build_symbol(variant: PostalVariant, segments: Vec<Segment>) -> Symbol {
Symbol::new(
variant.symbology(),
segments,
SymbolMeta::Postal(PostalMeta::new(variant)),
)
}
fn numeric_data(symbol: &Symbol) -> Result<Vec<u8>> {
symbol
.segments
.iter()
.find(|s| matches!(s.mode, Mode::Numeric))
.map(|s| s.data.clone())
.ok_or_else(|| Error::invalid_data("postal symbol has no numeric segment"))
}
fn alnum_data(symbol: &Symbol) -> Result<Vec<u8>> {
symbol
.segments
.iter()
.find(|s| matches!(s.mode, Mode::Numeric | Mode::Alphanumeric))
.map(|s| s.data.clone())
.ok_or_else(|| Error::invalid_data("postal symbol has no data segment"))
}
fn imb_fields(symbol: &Symbol) -> Result<(Vec<u8>, Vec<u8>)> {
let mut numerics = symbol
.segments
.iter()
.filter(|s| matches!(s.mode, Mode::Numeric));
let tracking = numerics
.next()
.map(|s| s.data.clone())
.ok_or_else(|| Error::invalid_data("IMb symbol missing tracking segment"))?;
let routing = numerics.next().map(|s| s.data.clone()).unwrap_or_default();
Ok((tracking, routing))
}
fn bars_to_matrix(bars: &[BarState]) -> BitMatrix {
let width = if bars.is_empty() {
0
} else {
2 * bars.len() - 1
};
let mut m = BitMatrix::new(width, ROWS, QUIET);
for (i, &bar) in bars.iter().enumerate() {
let col = i * 2;
if bar.has_ascender() {
m.set(col, 0, true);
}
m.set(col, 1, true); if bar.has_descender() {
m.set(col, 2, true);
}
}
m
}
fn matrix_to_bars(m: &BitMatrix) -> Result<Vec<BarState>> {
if m.height() != ROWS {
return Err(Error::undecodable("postal matrix must be 3 rows tall"));
}
let w = m.width();
if w == 0 || w.is_multiple_of(2) {
return Err(Error::undecodable("postal matrix width must be odd"));
}
let n = w.div_ceil(2);
let mut bars = Vec::with_capacity(n);
for i in 0..n {
let col = i * 2;
if !m.get(col, 1) {
return Err(Error::undecodable("postal bar missing its tracker module"));
}
bars.push(BarState::from_parts(m.get(col, 0), m.get(col, 2)));
let gap = col + 1;
if gap < w && (m.get(gap, 0) || m.get(gap, 1) || m.get(gap, 2)) {
return Err(Error::undecodable("postal gap column is not light"));
}
}
Ok(bars)
}