///|
/// Decoding failures for module-level barcode readers.
pub(all) enum DecodeError {
InvalidModules(String)
ChecksumMismatch(String)
} derive(Eq, Debug)
///|
pub fn decode_error_message(error : DecodeError) -> String {
match error {
InvalidModules(message) => "invalid modules: \{message}"
ChecksumMismatch(message) => "checksum mismatch: \{message}"
}
}
///|
fn module_pattern(modules : Array[Bool], start : Int, length : Int) -> String {
let out = Buffer()
for i = start; i < start + length; i = i + 1 {
out.write_byte(if modules[i] { b'1' } else { b'0' })
}
@utf8.decode_lossy(out.to_bytes())
}
///|
fn pattern_digit(pattern : String, table : Array[String]) -> Int? {
for i = 0; i < table.length(); i = i + 1 {
if pattern == table[i] {
return Some(i)
}
}
None
}
///|
fn parity_digit(pattern : String) -> Int? {
for i = 0; i < ean13_parity.length(); i = i + 1 {
if pattern == ean13_parity[i] {
return Some(i)
}
}
None
}
///|
fn append_decimal(out : Buffer, value : Int) -> Unit {
out.write_byte(b'0' + value.to_byte())
}
///|
/// Decode a canonical 95-module EAN-13 symbol and verify its check digit.
pub fn decode_ean13(modules : Array[Bool]) -> Result[String, DecodeError] {
if modules.length() != 95 {
return Err(InvalidModules("EAN-13 requires exactly 95 modules"))
}
if module_pattern(modules, 0, 3) != "101" ||
module_pattern(modules, 45, 5) != "01010" ||
module_pattern(modules, 92, 3) != "101" {
return Err(InvalidModules("EAN-13 guard pattern is invalid"))
}
let parity = Buffer()
let left_digits : Array[Int] = []
for i = 0; i < 6; i = i + 1 {
let pattern = module_pattern(modules, 3 + i * 7, 7)
match pattern_digit(pattern, l_code) {
Some(digit) => {
parity.write_byte(b'0')
left_digits.push(digit)
}
None =>
match pattern_digit(pattern, g_code) {
Some(digit) => {
parity.write_byte(b'1')
left_digits.push(digit)
}
None =>
return Err(
InvalidModules("EAN-13 left digit \{i + 1} has no valid pattern"),
)
}
}
}
let first = match parity_digit(@utf8.decode_lossy(parity.to_bytes())) {
Some(digit) => digit
None => return Err(InvalidModules("EAN-13 parity pattern is invalid"))
}
let out = Buffer()
append_decimal(out, first)
for digit in left_digits {
append_decimal(out, digit)
}
for i = 0; i < 6; i = i + 1 {
let pattern = module_pattern(modules, 50 + i * 7, 7)
match pattern_digit(pattern, r_code) {
Some(digit) => append_decimal(out, digit)
None =>
return Err(
InvalidModules("EAN-13 right digit \{i + 1} has no valid pattern"),
)
}
}
let data = @utf8.decode_lossy(out.to_bytes())
if !mod10_verify(data, 1, 3) {
return Err(ChecksumMismatch("EAN-13 check digit does not match"))
}
Ok(data)
}
///|
/// Decode a canonical 67-module EAN-8 symbol and verify its check digit.
pub fn decode_ean8(modules : Array[Bool]) -> Result[String, DecodeError] {
if modules.length() != 67 {
return Err(InvalidModules("EAN-8 requires exactly 67 modules"))
}
if module_pattern(modules, 0, 3) != "101" ||
module_pattern(modules, 31, 5) != "01010" ||
module_pattern(modules, 64, 3) != "101" {
return Err(InvalidModules("EAN-8 guard pattern is invalid"))
}
let out = Buffer()
for i = 0; i < 4; i = i + 1 {
match pattern_digit(module_pattern(modules, 3 + i * 7, 7), l_code) {
Some(digit) => append_decimal(out, digit)
None => return Err(InvalidModules("EAN-8 left digit \{i + 1} is invalid"))
}
}
for i = 0; i < 4; i = i + 1 {
match pattern_digit(module_pattern(modules, 36 + i * 7, 7), r_code) {
Some(digit) => append_decimal(out, digit)
None =>
return Err(InvalidModules("EAN-8 right digit \{i + 1} is invalid"))
}
}
let data = @utf8.decode_lossy(out.to_bytes())
if !mod10_verify(data, 3, 1) {
return Err(ChecksumMismatch("EAN-8 check digit does not match"))
}
Ok(data)
}
///|
/// Decode UPC-A through its standards-compatible EAN-13 representation.
pub fn decode_upca(modules : Array[Bool]) -> Result[String, DecodeError] {
match decode_ean13(modules) {
Err(error) => Err(error)
Ok(data) =>
if data.has_prefix("0") {
Ok(data.sub(start=1, end=data.length()).to_owned())
} else {
Err(InvalidModules("UPC-A EAN-13 prefix must be zero"))
}
}
}
///|
fn read_runs(modules : Array[Bool]) -> Array[(Bool, Int)] {
let runs : Array[(Bool, Int)] = []
if modules.is_empty() {
return runs
}
let mut dark = modules[0]
let mut width = 1
for i = 1; i < modules.length(); i = i + 1 {
if modules[i] == dark {
width = width + 1
} else {
runs.push((dark, width))
dark = modules[i]
width = 1
}
}
runs.push((dark, width))
runs
}
///|
fn itf_pattern_digit(pattern : String) -> Int? {
for i = 0; i < itf_digit_patterns.length(); i = i + 1 {
if pattern == itf_digit_patterns[i] {
return Some(i)
}
}
None
}
///|
fn runs_to_itf_pattern(runs : Array[(Bool, Int)], start : Int) -> String {
let out = Buffer()
for i = start; i < start + 10; i = i + 2 {
out.write_byte(if runs[i].1 == 3 { b'w' } else { b'n' })
}
@utf8.decode_lossy(out.to_bytes())
}
///|
/// Decode an ITF-14 module stream produced with the standard 3:1 wide ratio.
pub fn decode_itf14(modules : Array[Bool]) -> Result[String, DecodeError] {
let runs = read_runs(modules)
if runs.length() != 77 {
return Err(InvalidModules("ITF-14 requires 77 alternating elements"))
}
if runs[0] != (true, 1) ||
runs[1] != (false, 1) ||
runs[2] != (true, 1) ||
runs[3] != (false, 1) {
return Err(InvalidModules("ITF-14 start pattern is invalid"))
}
if runs[74] != (true, 3) || runs[75] != (false, 1) || runs[76] != (true, 1) {
return Err(InvalidModules("ITF-14 stop pattern is invalid"))
}
for i = 4; i < 74; i = i + 1 {
if runs[i].1 != 1 && runs[i].1 != 3 {
return Err(InvalidModules("ITF-14 element width must be 1 or 3"))
}
}
let out = Buffer()
for pair = 0; pair < 7; pair = pair + 1 {
let offset = 4 + pair * 10
let bars = runs_to_itf_pattern(runs, offset)
let spaces = runs_to_itf_pattern(runs, offset + 1)
match (itf_pattern_digit(bars), itf_pattern_digit(spaces)) {
(Some(left), Some(right)) => {
append_decimal(out, left)
append_decimal(out, right)
}
_ =>
return Err(InvalidModules("ITF-14 digit pair \{pair + 1} is invalid"))
}
}
let data = @utf8.decode_lossy(out.to_bytes())
if !mod10_verify(data, 3, 1) {
return Err(ChecksumMismatch("ITF-14 check digit does not match"))
}
Ok(data)
}
///|
fn code39_pattern_from_runs(runs : Array[(Bool, Int)], start : Int) -> String {
let out = Buffer()
for i = start; i < start + 9; i = i + 1 {
out.write_byte(if runs[i].1 == 3 { b'1' } else { b'0' })
}
@utf8.decode_lossy(out.to_bytes())
}
///|
fn code39_pattern_value(pattern : String) -> Int? {
for i = 0; i < code39_patterns.length(); i = i + 1 {
if pattern == code39_patterns[i] {
return Some(i)
}
}
None
}
///|
/// Decode a basic Code 39 symbol. When `verify_checksum` is true, the final
/// data character is treated as mod-43 checksum and removed from the result.
pub fn decode_code39(
modules : Array[Bool],
verify_checksum? : Bool = false,
) -> Result[String, DecodeError] {
let runs = read_runs(modules)
if runs.length() < 29 || (runs.length() - 19) % 10 != 0 {
return Err(InvalidModules("Code 39 element count is invalid"))
}
if code39_pattern_from_runs(runs, 0) != code39_star ||
code39_pattern_from_runs(runs, runs.length() - 9) != code39_star {
return Err(InvalidModules("Code 39 start or stop pattern is invalid"))
}
let count = (runs.length() - 19) / 10
let out = Buffer()
let charset = @utf8.encode(code39_charset)
for i = 0; i < count; i = i + 1 {
let separator = 9 + i * 10
if runs[separator] != (false, 1) {
return Err(InvalidModules("Code 39 inter-character gap is invalid"))
}
let pattern = code39_pattern_from_runs(runs, separator + 1)
match code39_pattern_value(pattern) {
Some(value) => out.write_byte(charset[value])
None =>
return Err(InvalidModules("Code 39 character \{i + 1} is invalid"))
}
}
if runs[runs.length() - 10] != (false, 1) {
return Err(InvalidModules("Code 39 final gap is invalid"))
}
let data = @utf8.decode_lossy(out.to_bytes())
if !verify_checksum {
return Ok(data)
}
if data.length() < 2 {
return Err(ChecksumMismatch("Code 39 checksum requires payload data"))
}
let payload = data.sub(start=0, end=data.length() - 1).to_owned()
let check = @utf8.encode(data)[data.length() - 1]
match code39_checksum(payload) {
Ok(value) =>
if charset[value] == check {
Ok(payload)
} else {
Err(ChecksumMismatch("Code 39 mod-43 checksum does not match"))
}
Err(message) => Err(ChecksumMismatch(message))
}
}