#![forbid(unsafe_code)]
use crate::common::{
buffer::SliceWriter, errors::EncodeError, traits::BarcodeEncoder, types::Encoded,
};
pub(crate) const L_CODE: [[bool; 7]; 10] = [
[false, false, false, true, true, false, true], [false, false, true, true, false, false, true], [false, false, true, false, false, true, true], [false, true, true, true, true, false, true], [false, true, false, false, false, true, true], [false, true, true, false, false, false, true], [false, true, false, true, true, true, true], [false, true, true, true, false, true, true], [false, true, true, false, true, true, true], [false, false, false, true, false, true, true], ];
const G_CODE: [[bool; 7]; 10] = [
[false, true, false, false, true, true, true], [false, true, true, false, false, true, true], [false, false, true, true, false, true, true], [false, true, false, false, false, false, true], [false, false, true, true, true, false, true], [false, true, true, true, false, false, true], [false, false, false, false, true, false, true], [false, false, true, false, false, false, true], [false, false, false, true, false, false, true], [false, false, true, false, true, true, true], ];
pub(crate) const R_CODE: [[bool; 7]; 10] = [
[true, true, true, false, false, true, false], [true, true, false, false, true, true, false], [true, true, false, true, true, false, false], [true, false, false, false, false, true, false], [true, false, true, true, true, false, false], [true, false, false, true, true, true, false], [true, false, true, false, false, false, false], [true, false, false, false, true, false, false], [true, false, false, true, false, false, false], [true, true, true, false, true, false, false], ];
const PARITY: [[bool; 6]; 10] = [
[false, false, false, false, false, false], [false, false, true, false, true, true], [false, false, true, true, false, true], [false, false, true, true, true, false], [false, true, false, false, true, true], [false, true, true, false, false, true], [false, true, true, true, false, false], [false, true, false, true, false, true], [false, true, false, true, true, false], [false, true, true, false, true, false], ];
pub(crate) const GUARD_NORMAL: [bool; 3] = [true, false, true];
pub(crate) const GUARD_CENTRE: [bool; 5] = [false, true, false, true, false];
pub struct Ean13;
impl BarcodeEncoder for Ean13 {
type Input = str;
fn encode_into(input: &str, buf: &mut [bool]) -> Result<Encoded, EncodeError> {
let digits = parse_and_validate(input)?;
let len = encode_bars(&digits, buf)?;
Ok(Encoded::Linear { len, height: 69 })
}
fn symbology_name() -> &'static str {
"EAN-13"
}
}
fn parse_and_validate(input: &str) -> Result<[u8; 13], EncodeError> {
let trimmed = input.trim();
if !trimmed.chars().all(|c| c.is_ascii_digit()) {
return Err(EncodeError::InvalidInput(
"EAN-13 input must contain digits only",
));
}
match trimmed.len() {
12 => {
let mut digits = [0u8; 13];
for (i, c) in trimmed.chars().enumerate() {
digits[i] = c as u8 - b'0';
}
digits[12] = check_digit(&digits[..12]);
Ok(digits)
}
13 => {
let mut digits = [0u8; 13];
for (i, c) in trimmed.chars().enumerate() {
digits[i] = c as u8 - b'0';
}
let expected = check_digit(&digits[..12]);
if digits[12] != expected {
return Err(EncodeError::InvalidInput("EAN-13 check digit mismatch"));
}
Ok(digits)
}
_ => Err(EncodeError::InvalidInput(
"EAN-13 input must be 12 or 13 digits",
)),
}
}
pub(crate) fn check_digit(digits: &[u8]) -> u8 {
let sum: u32 = digits
.iter()
.rev()
.enumerate()
.map(|(i, &d)| {
let weight = if i % 2 == 0 { 3u32 } else { 1u32 };
weight * d as u32
})
.sum();
((10 - (sum % 10)) % 10) as u8
}
fn encode_bars(digits: &[u8; 13], buf: &mut [bool]) -> Result<usize, EncodeError> {
let system = digits[0] as usize;
let parity = PARITY[system];
let mut w = SliceWriter::new(buf);
w.extend(GUARD_NORMAL.iter().copied())?;
for (pos, &d) in digits[1..=6].iter().enumerate() {
let pattern = if parity[pos] {
&G_CODE[d as usize]
} else {
&L_CODE[d as usize]
};
w.extend(pattern.iter().copied())?;
}
w.extend(GUARD_CENTRE.iter().copied())?;
for &d in &digits[7..=12] {
w.extend(R_CODE[d as usize].iter().copied())?;
}
w.extend(GUARD_NORMAL.iter().copied())?;
Ok(w.len())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_digit_known() {
let digits: [u8; 12] = [5, 9, 0, 1, 2, 3, 4, 1, 2, 3, 4, 5];
assert_eq!(check_digit(&digits), 7);
}
fn bars<'a>(input: &str, buf: &'a mut [bool]) -> &'a [bool] {
match Ean13::encode_into(input, buf).unwrap() {
Encoded::Linear { len, .. } => &buf[..len],
_ => panic!("expected linear"),
}
}
#[test]
fn test_encode_13_digits() {
let mut buf = [false; 128];
assert_eq!(bars("5901234123457", &mut buf).len(), 95);
}
#[test]
fn test_encode_12_digits_auto_check() {
let mut buf12 = [false; 128];
let mut buf13 = [false; 128];
assert_eq!(
bars("590123412345", &mut buf12),
bars("5901234123457", &mut buf13)
);
}
#[test]
fn test_invalid_check_digit() {
let mut buf = [false; 128];
assert!(Ean13::encode_into("5901234123458", &mut buf).is_err());
}
#[test]
fn test_invalid_characters() {
let mut buf = [false; 128];
assert!(Ean13::encode_into("590123412345X", &mut buf).is_err());
}
#[test]
fn test_wrong_length() {
let mut buf = [false; 128];
assert!(Ean13::encode_into("590123", &mut buf).is_err());
}
#[test]
fn test_buffer_too_small() {
let mut buf = [false; 32];
assert_eq!(
Ean13::encode_into("5901234123457", &mut buf),
Err(EncodeError::BufferTooSmall)
);
}
#[test]
fn test_symbology_name() {
assert_eq!(Ean13::symbology_name(), "EAN-13");
}
#[cfg(feature = "alloc")]
#[test]
fn test_svg_output_contains_svg_tag() {
let svg = Ean13::encode("5901234123457").unwrap().to_svg_string();
assert!(svg.starts_with("<svg "));
assert!(svg.contains("</svg>"));
}
}