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 QUIET_ZONE: usize = 0;
const DATA_LEN_FN: usize = 23;
const DATA_LEN_NO_FN: usize = 15;
#[derive(Debug, Clone, PartialEq, Eq)]
struct DxData {
part1: u8,
part2: u8,
frame: Option<(u8, bool)>,
}
impl DxData {
fn data_len(&self) -> usize {
if self.frame.is_some() {
DATA_LEN_FN
} else {
DATA_LEN_NO_FN
}
}
fn to_text(&self) -> String {
let mut s = format!("{}-{}", self.part1, self.part2);
if let Some((frame, half)) = self.frame {
s.push('/');
s.push_str(&frame.to_string());
if half {
s.push('A');
}
}
s
}
fn parse(text: &str) -> Result<DxData> {
let (p1, rest) = text
.split_once('-')
.ok_or_else(|| Error::invalid_data("DX number must contain '-'"))?;
let part1: u8 = p1
.parse()
.map_err(|_| Error::invalid_data("DX part 1 is not a number"))?;
if part1 == 0 || part1 > 127 {
return Err(Error::capacity("DX part 1 must be 1..=127"));
}
let (p2_str, frame) = match rest.split_once('/') {
Some((p2, frame_str)) => {
let (digits, half) = match frame_str.strip_suffix('A') {
Some(d) => (d, true),
None => (frame_str, false),
};
let frame: u8 = digits
.parse()
.map_err(|_| Error::invalid_data("frame number is not a number"))?;
if frame > 63 {
return Err(Error::capacity("frame number must be 0..=63"));
}
(p2, Some((frame, half)))
}
None => (rest, None),
};
let part2: u8 = p2_str
.parse()
.map_err(|_| Error::invalid_data("DX part 2 is not a number"))?;
if part2 > 15 {
return Err(Error::capacity("DX part 2 must be 0..=15"));
}
Ok(DxData {
part1,
part2,
frame,
})
}
fn data_bits(&self) -> Vec<bool> {
let mut bits = vec![false; self.data_len()];
write_field(&mut bits, 1, 7, self.part1 as u32); write_field(&mut bits, 9, 4, self.part2 as u32); if let Some((frame, half)) = self.frame {
write_field(&mut bits, 13, 6, frame as u32);
bits[19] = half;
bits[21] = even_parity(&bits[..21]);
} else {
bits[13] = even_parity(&bits[..13]);
}
bits
}
}
fn write_field(bits: &mut [bool], start: usize, len: usize, value: u32) {
for i in 0..len {
bits[start + i] = (value >> (len - 1 - i)) & 1 != 0;
}
}
fn read_field(bits: &[bool], start: usize, len: usize) -> u32 {
let mut v = 0u32;
for i in 0..len {
v = (v << 1) | bits[start + i] as u32;
}
v
}
fn even_parity(bits: &[bool]) -> bool {
bits.iter().filter(|&&b| b).count() % 2 == 1
}
fn clock_row(width: usize) -> Vec<bool> {
let mut row = vec![false; width];
let mut x = 0usize;
let mut dark = true;
for run in run_lengths(width) {
if dark {
for m in row.iter_mut().skip(x).take(run) {
*m = true;
}
}
x += run;
dark = !dark;
}
row
}
fn run_lengths(width: usize) -> Vec<usize> {
let mut runs = vec![5];
runs.extend(std::iter::repeat_n(1, width - 5 - 3));
runs.push(3);
runs
}
fn render(dx: &DxData) -> BitMatrix {
let data = dx.data_bits();
let width = 5 + data.len() + 3;
let mut m = BitMatrix::new(width, 2, QUIET_ZONE);
for (x, &dark) in clock_row(width).iter().enumerate() {
if dark {
m.set(x, 0, true);
}
}
for x in [0usize, 2, 4] {
m.set(x, 1, true);
}
for (i, &bit) in data.iter().enumerate() {
if bit {
m.set(5 + i, 1, true);
}
}
m.set(5 + data.len(), 1, true);
m.set(5 + data.len() + 2, 1, true);
m
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DxFilmMeta {
pub has_frame_number: bool,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DxFilmEncoder;
impl DxFilmEncoder {
pub fn new() -> Self {
Self
}
pub fn build(&self, part1: u8, part2: u8, frame: Option<(u8, bool)>) -> Result<Symbol> {
let dx = DxData {
part1,
part2,
frame,
};
let text = dx.to_text();
let dx = DxData::parse(&text)?;
Ok(Symbol::new(
Symbology::DxFilmEdge,
vec![Segment::byte(text.into_bytes())],
SymbolMeta::DxFilm(DxFilmMeta {
has_frame_number: dx.frame.is_some(),
}),
))
}
pub fn build_text(&self, text: &str) -> Result<Symbol> {
let dx = DxData::parse(text)?;
self.build(dx.part1, dx.part2, dx.frame)
}
}
impl Encode for DxFilmEncoder {
fn encode(&self, symbol: &Symbol) -> Result<Encoding> {
if symbol.symbology != Symbology::DxFilmEdge {
return Err(Error::invalid_parameter(
"DxFilmEncoder given a non-DX-Film-Edge symbol",
));
}
let text = flatten_text(&symbol.segments)?;
let dx = DxData::parse(&text)?;
Ok(Encoding::Matrix(render(&dx)))
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DxFilmDecoder;
impl DxFilmDecoder {
pub fn new() -> Self {
Self
}
pub fn decode_matrix(&self, m: &BitMatrix) -> Result<Symbol> {
if m.height() != 2 {
return Err(Error::undecodable("DX Film Edge grid must have two rows"));
}
let width = m.width();
let data_len = match width.checked_sub(5 + 3) {
Some(DATA_LEN_FN) => DATA_LEN_FN,
Some(DATA_LEN_NO_FN) => DATA_LEN_NO_FN,
_ => return Err(Error::undecodable("DX Film Edge width is not valid")),
};
let has_fn = data_len == DATA_LEN_FN;
for (x, &dark) in clock_row(width).iter().enumerate() {
if m.get(x, 0) != dark {
return Err(Error::undecodable("DX Film Edge clock track mismatch"));
}
}
let start_ok = (0..5).all(|x| m.get(x, 1) == matches!(x, 0 | 2 | 4));
if !start_ok {
return Err(Error::undecodable("DX Film Edge start pattern mismatch"));
}
let stop = 5 + data_len;
if !m.get(stop, 1) || m.get(stop + 1, 1) || !m.get(stop + 2, 1) {
return Err(Error::undecodable("DX Film Edge stop pattern mismatch"));
}
let bits: Vec<bool> = (0..data_len).map(|i| m.get(5 + i, 1)).collect();
let sep_ok = if has_fn {
!bits[0] && !bits[8] && !bits[20] && !bits[22]
} else {
!bits[0] && !bits[8] && !bits[14]
};
if !sep_ok {
return Err(Error::undecodable("DX Film Edge separator is not light"));
}
let parity_index = data_len - 2;
if even_parity(&bits[..parity_index]) != bits[parity_index] {
return Err(Error::undecodable("DX Film Edge parity mismatch"));
}
let part1 = read_field(&bits, 1, 7) as u8;
if part1 == 0 {
return Err(Error::undecodable("DX Film Edge part 1 is zero"));
}
let part2 = read_field(&bits, 9, 4) as u8;
let frame = has_fn.then(|| (read_field(&bits, 13, 6) as u8, bits[19]));
let dx = DxData {
part1,
part2,
frame,
};
Ok(Symbol::new(
Symbology::DxFilmEdge,
vec![Segment::byte(dx.to_text().into_bytes())],
SymbolMeta::DxFilm(DxFilmMeta {
has_frame_number: has_fn,
}),
))
}
}
impl Decode for DxFilmDecoder {
fn decode(&self, encoding: &Encoding) -> Result<Symbol> {
match encoding {
Encoding::Matrix(m) => self.decode_matrix(m),
Encoding::Linear(_) => Err(Error::Unsupported {
what: "DX Film Edge decode of a linear pattern",
}),
}
}
}
fn flatten_text(segments: &[Segment]) -> Result<String> {
let mut out = Vec::new();
for seg in segments {
if matches!(seg.mode, Mode::Eci(_)) {
return Err(Error::Unsupported {
what: "DX Film Edge ECI segments",
});
}
out.extend_from_slice(&seg.data);
}
String::from_utf8(out).map_err(|_| Error::invalid_data("DX Film Edge payload is not UTF-8"))
}