#[derive(Debug, Clone)]
pub enum Encoded {
Linear(LinearPattern),
Matrix(BitMatrix),
Postal4State(Postal4Pattern),
Stacked(StackedPattern),
Dots(DotMatrix),
Hex(Box<crate::symbology::maxicode::MaxiCodeSymbol>),
ColorMatrix(ColorMatrix),
}
#[derive(Debug, Clone)]
pub struct ColorMatrix {
width: usize,
height: usize,
cells: Vec<u8>,
palette: [Rgb8; 8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rgb8 {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb8 {
#[inline]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
pub fn to_css_hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
impl ColorMatrix {
pub fn new(width: usize, height: usize, palette: [Rgb8; 8]) -> Self {
Self {
width,
height,
cells: vec![0; width * height],
palette,
}
}
#[inline]
pub fn width(&self) -> usize {
self.width
}
#[inline]
pub fn height(&self) -> usize {
self.height
}
#[inline]
pub fn get(&self, x: usize, y: usize) -> u8 {
self.cells[y * self.width + x]
}
#[inline]
pub fn set(&mut self, x: usize, y: usize, palette_idx: u8) {
self.cells[y * self.width + x] = palette_idx & 0b111;
}
#[inline]
pub fn palette(&self) -> &[Rgb8; 8] {
&self.palette
}
#[inline]
pub fn cell_color(&self, x: usize, y: usize) -> Rgb8 {
self.palette[self.get(x, y) as usize]
}
}
#[derive(Debug, Clone)]
pub struct DotMatrix {
width: usize,
height: usize,
data: Vec<bool>,
}
impl DotMatrix {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
data: vec![false; width * height],
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn get(&self, x: usize, y: usize) -> bool {
self.data[y * self.width + x]
}
pub fn set(&mut self, x: usize, y: usize, value: bool) {
self.data[y * self.width + x] = value;
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Bar4State {
Tracker,
Descender,
Ascender,
Full,
}
impl Bar4State {
pub fn from_digit(d: u8) -> Option<Self> {
Some(match d {
0 => Self::Tracker,
1 => Self::Descender,
2 => Self::Ascender,
3 => Self::Full,
_ => return None,
})
}
pub fn has_ascender(self) -> bool {
matches!(self, Self::Ascender | Self::Full)
}
pub fn has_descender(self) -> bool {
matches!(self, Self::Descender | Self::Full)
}
}
#[derive(Debug, Clone)]
pub struct Postal4Pattern {
pub bars: Vec<Bar4State>,
pub text: Option<String>,
}
#[derive(Debug, Clone)]
pub struct StackedPattern {
pub rows: Vec<LinearPattern>,
pub text: Option<String>,
}
impl StackedPattern {
pub fn new(rows: Vec<LinearPattern>, text: Option<String>) -> Result<Self, (u32, u32)> {
if rows.is_empty() {
return Ok(Self { rows, text });
}
let w = rows[0].total_width();
for r in &rows[1..] {
let rw = r.total_width();
if rw != w {
return Err((w, rw));
}
}
Ok(Self { rows, text })
}
pub fn width(&self) -> u32 {
self.rows.first().map(|r| r.total_width()).unwrap_or(0)
}
pub fn height_rows(&self) -> usize {
self.rows.len()
}
}
impl Postal4Pattern {
pub fn from_digits(digits: &[u8], text: Option<String>) -> Option<Self> {
let mut bars = Vec::with_capacity(digits.len());
for &d in digits {
bars.push(Bar4State::from_digit(d)?);
}
Some(Self { bars, text })
}
pub fn len(&self) -> usize {
self.bars.len()
}
pub fn is_empty(&self) -> bool {
self.bars.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct LinearPattern {
pub bars: Vec<u8>,
pub text: Option<String>,
}
impl LinearPattern {
pub fn from_modules(modules: &str, text: Option<String>) -> Self {
let mut bars = Vec::new();
let chars: Vec<char> = modules.chars().collect();
if chars.is_empty() {
return Self { bars, text };
}
let mut expected = '1';
let mut run: u8 = 0;
for c in chars {
if c == expected {
run += 1;
} else {
bars.push(run);
run = 1;
expected = if expected == '1' { '0' } else { '1' };
}
}
bars.push(run);
Self { bars, text }
}
pub fn total_width(&self) -> u32 {
self.bars.iter().map(|&b| u32::from(b)).sum()
}
}
#[derive(Debug, Clone)]
pub struct BitMatrix {
width: usize,
height: usize,
data: Vec<bool>,
}
impl BitMatrix {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
data: vec![false; width * height],
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn get(&self, x: usize, y: usize) -> bool {
self.data[y * self.width + x]
}
pub fn set(&mut self, x: usize, y: usize, value: bool) {
self.data[y * self.width + x] = value;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_modules_run_length_encodes() {
let p = LinearPattern::from_modules("111001100011", None);
assert_eq!(p.bars, vec![3, 2, 2, 3, 2]);
assert_eq!(p.total_width(), 12);
}
#[test]
fn from_modules_edge_cases() {
let p = LinearPattern::from_modules("", None);
assert!(p.bars.is_empty(), "empty input → empty bars");
assert_eq!(p.total_width(), 0);
let p = LinearPattern::from_modules("1", None);
assert_eq!(p.bars, vec![1]);
let p = LinearPattern::from_modules("0", None);
assert_eq!(
p.bars,
vec![0, 1],
"single '0' inserts a 0-width bar to preserve alternation"
);
let p = LinearPattern::from_modules("11", None);
assert_eq!(p.bars, vec![2]);
let p = LinearPattern::from_modules("101", None);
assert_eq!(p.bars, vec![1, 1, 1]);
let p = LinearPattern::from_modules("0011", None);
assert_eq!(p.bars, vec![0, 2, 2]);
let p = LinearPattern::from_modules("100", None);
assert_eq!(p.bars, vec![1, 2]);
let p = LinearPattern::from_modules("0010", None);
assert_eq!(p.bars, vec![0, 2, 1, 1]);
let p = LinearPattern::from_modules("10101", None);
assert_eq!(p.bars, vec![1, 1, 1, 1, 1]);
let p = LinearPattern::from_modules("1", Some("X".into()));
assert_eq!(p.text.as_deref(), Some("X"));
}
#[test]
fn matrix_default_white_and_set_works() {
let mut m = BitMatrix::new(5, 3);
assert!(!m.get(2, 1));
m.set(2, 1, true);
assert!(m.get(2, 1));
assert!(!m.get(0, 0));
}
#[test]
fn dot_matrix_new_set_get() {
let mut m = DotMatrix::new(7, 4);
assert_eq!(m.width(), 7);
assert_eq!(m.height(), 4);
for y in 0..4 {
for x in 0..7 {
assert!(!m.get(x, y), "({x},{y}) should default false");
}
}
m.set(3, 2, true);
assert!(m.get(3, 2));
for y in 0..4 {
for x in 0..7 {
let expected = (x, y) == (3, 2);
assert_eq!(m.get(x, y), expected, "({x},{y})");
}
}
m.set(0, 0, true);
assert!(m.get(0, 0));
assert!(m.get(3, 2));
m.set(3, 2, false);
assert!(!m.get(3, 2));
assert!(m.get(0, 0)); }
#[test]
fn bar4state_from_digit_round_trip() {
assert_eq!(Bar4State::from_digit(0), Some(Bar4State::Tracker));
assert_eq!(Bar4State::from_digit(1), Some(Bar4State::Descender));
assert_eq!(Bar4State::from_digit(2), Some(Bar4State::Ascender));
assert_eq!(Bar4State::from_digit(3), Some(Bar4State::Full));
assert_eq!(Bar4State::from_digit(4), None);
}
#[test]
fn bar4state_ascender_descender_flags() {
assert!(Bar4State::Ascender.has_ascender());
assert!(Bar4State::Full.has_ascender());
assert!(!Bar4State::Descender.has_ascender());
assert!(!Bar4State::Tracker.has_ascender());
assert!(Bar4State::Descender.has_descender());
assert!(Bar4State::Full.has_descender());
assert!(!Bar4State::Ascender.has_descender());
assert!(!Bar4State::Tracker.has_descender());
}
#[test]
fn postal4_pattern_from_digits_rejects_out_of_range() {
assert!(Postal4Pattern::from_digits(&[0, 1, 2, 4], None).is_none());
let p = Postal4Pattern::from_digits(&[3, 2, 1, 0], None).unwrap();
assert_eq!(p.len(), 4);
assert_eq!(
p.bars,
vec![
Bar4State::Full,
Bar4State::Ascender,
Bar4State::Descender,
Bar4State::Tracker
]
);
}
#[test]
fn postal4_pattern_len_and_is_empty() {
let empty = Postal4Pattern::from_digits(&[], None).unwrap();
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
let one = Postal4Pattern::from_digits(&[0], None).unwrap();
assert_eq!(one.len(), 1);
assert!(!one.is_empty(), "1-bar pattern must NOT be empty");
let four = Postal4Pattern::from_digits(&[3, 2, 1, 0], None).unwrap();
assert_eq!(four.len(), 4);
assert!(!four.is_empty());
let direct = Postal4Pattern {
bars: vec![],
text: Some("ignored".into()),
};
assert!(
direct.is_empty(),
"empty bars → is_empty regardless of text"
);
assert_eq!(direct.len(), 0);
}
#[test]
fn stacked_pattern_new_width_uniformity() {
let empty: Vec<LinearPattern> = Vec::new();
let sp = StackedPattern::new(empty, None).unwrap();
assert_eq!(sp.height_rows(), 0);
assert_eq!(sp.width(), 0);
let single = vec![LinearPattern {
bars: vec![1, 2, 3, 4],
text: None,
}];
let sp = StackedPattern::new(single, Some("hi".into())).unwrap();
assert_eq!(sp.height_rows(), 1);
assert_eq!(sp.width(), 10); assert_eq!(sp.text.as_deref(), Some("hi"));
let uniform = vec![
LinearPattern {
bars: vec![1, 2, 3],
text: None,
},
LinearPattern {
bars: vec![3, 2, 1],
text: None,
},
LinearPattern {
bars: vec![2, 2, 2],
text: None,
},
];
let sp = StackedPattern::new(uniform, None).unwrap();
assert_eq!(sp.height_rows(), 3);
assert_eq!(sp.width(), 6);
let mismatch = vec![
LinearPattern {
bars: vec![1, 2, 3],
text: None,
}, LinearPattern {
bars: vec![1, 2, 4],
text: None,
}, ];
match StackedPattern::new(mismatch, None) {
Err((expected, got)) => {
assert_eq!(expected, 6, "first row's width is the 'expected'");
assert_eq!(got, 7, "diverging row's width is 'got'");
}
Ok(_) => panic!("width mismatch should error"),
}
let late_mismatch = vec![
LinearPattern {
bars: vec![1, 2, 3],
text: None,
}, LinearPattern {
bars: vec![3, 2, 1],
text: None,
}, LinearPattern {
bars: vec![5, 5, 5],
text: None,
}, ];
match StackedPattern::new(late_mismatch, None) {
Err((expected, got)) => {
assert_eq!((expected, got), (6, 15));
}
Ok(_) => panic!("late mismatch must error"),
}
}
#[test]
fn rgb8_constructor_and_css_hex() {
let c = Rgb8::new(0x12, 0xab, 0xff);
assert_eq!((c.r, c.g, c.b), (0x12, 0xab, 0xff));
assert_eq!(c.to_css_hex(), "#12abff");
assert_eq!(Rgb8::new(0, 0, 0).to_css_hex(), "#000000");
assert_eq!(Rgb8::new(0x0a, 0, 0x0f).to_css_hex(), "#0a000f");
}
#[test]
fn color_matrix_new_set_get_cell_color() {
let palette: [Rgb8; 8] = [
Rgb8::new(0xff, 0xff, 0xff),
Rgb8::new(0x10, 0x20, 0x30),
Rgb8::new(0x40, 0x50, 0x60),
Rgb8::new(0x70, 0x80, 0x90),
Rgb8::new(0xa0, 0xb0, 0xc0),
Rgb8::new(0xd0, 0xe0, 0xf0),
Rgb8::new(0x01, 0x02, 0x03),
Rgb8::new(0x00, 0x00, 0x00),
];
let mut m = ColorMatrix::new(4, 3, palette);
assert_eq!(m.width(), 4);
assert_eq!(m.height(), 3);
assert_eq!(m.get(0, 0), 0);
assert_eq!(m.get(3, 2), 0);
assert_eq!(m.cell_color(0, 0), palette[0]);
m.set(0, 0, 5);
m.set(3, 2, 7);
m.set(1, 1, 2);
assert_eq!(m.get(0, 0), 5);
assert_eq!(m.get(3, 2), 7);
assert_eq!(m.get(1, 1), 2);
assert_eq!(m.cell_color(0, 0), palette[5]);
assert_eq!(m.cell_color(3, 2), palette[7]);
assert_eq!(m.cell_color(1, 1), palette[2]);
}
#[test]
fn color_matrix_set_masks_out_of_range_index() {
let palette: [Rgb8; 8] = [Rgb8::new(0, 0, 0); 8];
let mut m = ColorMatrix::new(1, 1, palette);
m.set(0, 0, 9); assert_eq!(m.get(0, 0), 1);
m.set(0, 0, 0xff); assert_eq!(m.get(0, 0), 7);
m.set(0, 0, 0x08); assert_eq!(m.get(0, 0), 0);
}
#[test]
fn color_matrix_palette_borrow() {
let palette: [Rgb8; 8] = [
Rgb8::new(0x11, 0x22, 0x33),
Rgb8::new(0x44, 0x55, 0x66),
Rgb8::new(0x77, 0x88, 0x99),
Rgb8::new(0xaa, 0xbb, 0xcc),
Rgb8::new(0xdd, 0xee, 0xff),
Rgb8::new(0x00, 0x11, 0x22),
Rgb8::new(0x33, 0x44, 0x55),
Rgb8::new(0x66, 0x77, 0x88),
];
let m = ColorMatrix::new(2, 2, palette);
let borrowed = m.palette();
assert_eq!(borrowed[0], palette[0]);
assert_eq!(borrowed[7], palette[7]);
assert_eq!(borrowed.len(), 8);
}
#[test]
fn rgb8_to_css_hex_known_colors() {
assert_eq!(Rgb8::new(0, 0, 0).to_css_hex(), "#000000");
assert_eq!(Rgb8::new(255, 255, 255).to_css_hex(), "#ffffff");
assert_eq!(Rgb8::new(255, 0, 0).to_css_hex(), "#ff0000");
assert_eq!(Rgb8::new(0, 255, 0).to_css_hex(), "#00ff00");
assert_eq!(Rgb8::new(0, 0, 255).to_css_hex(), "#0000ff");
assert_eq!(Rgb8::new(0xAB, 0xCD, 0xEF).to_css_hex(), "#abcdef");
assert_eq!(Rgb8::new(1, 2, 3).to_css_hex(), "#010203");
}
#[test]
fn color_matrix_set_masks_to_three_bits() {
let palette = [Rgb8::new(0, 0, 0); 8];
let mut cm = ColorMatrix::new(2, 2, palette);
cm.set(0, 0, 5);
assert_eq!(cm.get(0, 0), 5);
cm.set(1, 0, 7);
assert_eq!(cm.get(1, 0), 7);
cm.set(0, 1, 8); assert_eq!(cm.get(0, 1), 0);
cm.set(1, 1, 0xFF); assert_eq!(cm.get(1, 1), 7);
}
#[test]
fn color_matrix_cell_color_resolves_palette() {
let palette = [
Rgb8::new(0, 0, 0),
Rgb8::new(255, 0, 0),
Rgb8::new(0, 255, 0),
Rgb8::new(0, 0, 255),
Rgb8::new(255, 255, 0),
Rgb8::new(255, 0, 255),
Rgb8::new(0, 255, 255),
Rgb8::new(255, 255, 255),
];
let mut cm = ColorMatrix::new(1, 1, palette);
cm.set(0, 0, 3);
assert_eq!(cm.cell_color(0, 0), Rgb8::new(0, 0, 255));
cm.set(0, 0, 5);
assert_eq!(cm.cell_color(0, 0), Rgb8::new(255, 0, 255));
}
}