use crate::model::DecodedBarcode; pub fn extract_barcodes(bytes: &[u8]) -> Vec { extract_from_zpl_bytes(bytes) } pub fn extract_from_zpl(input: &str) -> Vec { extract_from_zpl_bytes(input.as_bytes()) } fn extract_from_zpl_bytes(bytes: &[u8]) -> Vec { let mut index = 0usize; let mut pending_format: Option = None; let mut barcodes = Vec::new(); while index + 2 < bytes.len() { if bytes[index] != b'^' && bytes[index] != b'~' { index += 1; continue; } let command_start = index; let command = &bytes[index + 1..index + 3]; index += 3; if let Some(format) = barcode_format(command) { pending_format = Some(format.to_string()); continue; } if command == b"FD" { let end = find_bytes(&bytes[index..], b"^FS") .map(|offset| index + offset) .unwrap_or(bytes.len()); if let Some(format) = pending_format.take() { let raw = String::from_utf8_lossy(&bytes[index..end]); let value = normalize_field_data(raw.trim()); if !value.is_empty() { barcodes.push(DecodedBarcode { format, value }); } } index = end.saturating_add(3); continue; } if command == b"FS" || command == b"XZ" { pending_format = None; } if index <= command_start { index = command_start + 1; } } barcodes } fn barcode_format(command: &[u8]) -> Option<&'static str> { match command { b"BC" => Some("CODE_128"), b"B3" => Some("CODE_39"), b"BE" => Some("EAN_13"), b"B8" => Some("EAN_8"), b"BU" => Some("UPC_A"), b"B9" => Some("UPC_E"), b"B2" => Some("ITF"), b"BQ" => Some("QR_CODE"), b"BX" => Some("DATA_MATRIX"), b"B7" => Some("PDF_417"), b"BA" => Some("CODE_93"), _ => None, } } fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { if needle.is_empty() { return Some(0); } haystack.windows(needle.len()).position(|window| window == needle) } fn normalize_field_data(raw: &str) -> String { let mut value = raw.to_string(); // Zebra Code 128 control sequences commonly select code sets or emit FNC1. for prefix in [">;", ">:", ">9"] { if value.starts_with(prefix) { value = value[prefix.len()..].to_string(); break; } } value = value.replace(">8", "\u{001d}"); // ^FH defaults to '_' as the hex escape character. Decode common escaped bytes. let hex = regex::Regex::new(r"_([0-9A-Fa-f]{2})").expect("valid regex"); hex.replace_all(&value, |caps: ®ex::Captures<'_>| { u8::from_str_radix(&caps[1], 16) .ok() .map(char::from) .unwrap_or('_') .to_string() }) .into_owned() } #[cfg(test)] mod tests { use super::*; #[test] fn extracts_multiple_zpl_barcode_types() { let zpl = "^XA^FO10,10^BCN,80,Y,N,N^FD>;0123456789012^FS^FO10,100^BQN,2,5^FDLA,HELLO^FS^XZ"; let found = extract_from_zpl(zpl); assert_eq!( found, vec![ DecodedBarcode { format: "CODE_128".into(), value: "0123456789012".into() }, DecodedBarcode { format: "QR_CODE".into(), value: "LA,HELLO".into() }, ] ); } #[test] fn preserves_gs1_separator_for_fnc1() { let zpl = "^XA^BCN,80,N,N,N^FD>801095011015300310ABC^FS^XZ"; let found = extract_from_zpl(zpl); assert_eq!(found[0].value, "\u{001d}01095011015300310ABC"); } #[test] fn ignores_normal_text_fields() { let zpl = "^XA^FO10,10^A0N,30,30^FDNOT-A-BARCODE^FS^XZ"; assert!(extract_from_zpl(zpl).is_empty()); } #[test] fn arbitrary_binary_printer_data_never_panics() { let payload = [ 0x1b, b'@', 0xff, 0x00, 0xfe, b'^', 0xff, 0xfd, 0x1b, b'(', b'G', 0x10, 0x00, 0x80, 0x81, 0x82, b'^', b'F', b'D', 0xf0, 0x9f, 0x92, 0xa9, b'^', b'F', b'S', ]; let found = extract_barcodes(&payload); assert!(found.is_empty()); } }