use anyhow::{anyhow, bail, Context, Result};
use rxing::BarcodeFormat;
use crate::cli::Args;
fn parse_format(s: &str) -> Result<BarcodeFormat> {
match s.trim().to_ascii_lowercase().as_str() {
"qr" | "qr_code" | "qrcode" => Ok(BarcodeFormat::QR_CODE),
"datamatrix" | "data_matrix" | "dm" => Ok(BarcodeFormat::DATA_MATRIX),
"aztec" => Ok(BarcodeFormat::AZTEC),
"pdf417" | "pdf_417" => Ok(BarcodeFormat::PDF_417),
"maxicode" => Ok(BarcodeFormat::MAXICODE),
"code128" | "code_128" => Ok(BarcodeFormat::CODE_128),
"code39" | "code_39" => Ok(BarcodeFormat::CODE_39),
"code93" | "code_93" => Ok(BarcodeFormat::CODE_93),
"codabar" => Ok(BarcodeFormat::CODABAR),
"ean13" | "ean_13" => Ok(BarcodeFormat::EAN_13),
"ean8" | "ean_8" => Ok(BarcodeFormat::EAN_8),
"itf" => Ok(BarcodeFormat::ITF),
"upca" | "upc_a" => Ok(BarcodeFormat::UPC_A),
"upce" | "upc_e" => Ok(BarcodeFormat::UPC_E),
"rss14" | "rss_14" => Ok(BarcodeFormat::RSS_14),
"rss_expanded" | "rssexpanded" => Ok(BarcodeFormat::RSS_EXPANDED),
other => bail!("unknown --decode-hints format '{other}' (use `recon --help decode` for the full list)"),
}
}
pub(crate) fn format_name(fmt: &BarcodeFormat) -> &'static str {
match fmt {
BarcodeFormat::QR_CODE => "qr",
BarcodeFormat::DATA_MATRIX => "datamatrix",
BarcodeFormat::AZTEC => "aztec",
BarcodeFormat::PDF_417 => "pdf417",
BarcodeFormat::MAXICODE => "maxicode",
BarcodeFormat::CODE_128 => "code128",
BarcodeFormat::CODE_39 => "code39",
BarcodeFormat::CODE_93 => "code93",
BarcodeFormat::CODABAR => "codabar",
BarcodeFormat::EAN_13 => "ean13",
BarcodeFormat::EAN_8 => "ean8",
BarcodeFormat::ITF => "itf",
BarcodeFormat::UPC_A => "upca",
BarcodeFormat::UPC_E => "upce",
BarcodeFormat::RSS_14 => "rss14",
BarcodeFormat::RSS_EXPANDED => "rss_expanded",
BarcodeFormat::MICRO_QR_CODE => "micro_qr",
BarcodeFormat::RECTANGULAR_MICRO_QR_CODE => "rmqr",
BarcodeFormat::TELEPEN => "telepen",
BarcodeFormat::DXFilmEdge => "dxfilmedge",
BarcodeFormat::UPC_EAN_EXTENSION => "upc_ean_extension",
BarcodeFormat::UNSUPORTED_FORMAT => "unsupported",
}
}
#[derive(Debug, Clone)]
pub struct Decoded {
pub text: String,
pub format: &'static str,
}
pub fn decode_file(path: &str, hints: &[BarcodeFormat]) -> Result<Decoded> {
let result = if hints.is_empty() {
rxing::helpers::detect_in_file(path, None)
} else if hints.len() == 1 {
rxing::helpers::detect_in_file(path, Some(hints[0]))
} else {
rxing::helpers::detect_in_file(path, None)
}
.map_err(|e| anyhow!("decode error: {e:?}"))?;
let fmt = *result.getBarcodeFormat();
if !hints.is_empty() && !hints.contains(&fmt) {
bail!(
"decoded a {} barcode but --decode-hints restricted to {:?}",
format_name(&fmt),
hints.iter().map(format_name).collect::<Vec<_>>(),
);
}
Ok(Decoded {
text: result.getText().to_string(),
format: format_name(&fmt),
})
}
pub fn decode_all_file(path: &str) -> Result<Vec<Decoded>> {
let results = rxing::helpers::detect_multiple_in_file(path)
.map_err(|e| anyhow!("decode_all error: {e:?}"))?;
if results.is_empty() {
bail!("no barcodes detected in '{path}'");
}
Ok(results
.into_iter()
.map(|r| Decoded {
text: r.getText().to_string(),
format: format_name(r.getBarcodeFormat()),
})
.collect())
}
fn sniff_image_suffix(bytes: &[u8]) -> &'static str {
if bytes.len() >= 8 && &bytes[..8] == b"\x89PNG\r\n\x1a\n" {
".png"
} else if bytes.len() >= 3 && &bytes[..3] == b"\xff\xd8\xff" {
".jpg"
} else if bytes.len() >= 6
&& (&bytes[..6] == b"GIF87a" || &bytes[..6] == b"GIF89a")
{
".gif"
} else if bytes.len() >= 12
&& &bytes[..4] == b"RIFF"
&& &bytes[8..12] == b"WEBP"
{
".webp"
} else if bytes.len() >= 2 && &bytes[..2] == b"BM" {
".bmp"
} else if bytes.len() >= 4 && (&bytes[..4] == b"II*\0" || &bytes[..4] == b"MM\0*") {
".tiff"
} else {
".png"
}
}
pub fn decode_all_bytes(bytes: &[u8]) -> Result<Vec<Decoded>> {
use std::io::Write;
let mut tmp = tempfile::Builder::new()
.prefix("recon-decode-all-")
.suffix(sniff_image_suffix(bytes))
.tempfile()
.context("decode_all: create tempfile")?;
tmp.write_all(bytes).context("decode_all: write tempfile")?;
tmp.flush().ok();
let path = tmp
.path()
.to_str()
.ok_or_else(|| anyhow!("decode_all: tempfile path is not UTF-8"))?;
decode_all_file(path)
}
pub fn decode_bytes(bytes: &[u8], hints: &[BarcodeFormat]) -> Result<Decoded> {
use std::io::Write;
let mut tmp = tempfile::Builder::new()
.prefix("recon-decode-")
.suffix(sniff_image_suffix(bytes))
.tempfile()
.context("decode: create tempfile")?;
tmp.write_all(bytes).context("decode: write tempfile")?;
tmp.flush().ok();
let path = tmp
.path()
.to_str()
.ok_or_else(|| anyhow!("decode: tempfile path is not UTF-8"))?;
decode_file(path, hints)
}
pub fn run_all(args: &Args) -> Result<()> {
let src = args
.decode_all
.as_ref()
.context("--decode-all requires an image path (or `-` for stdin)")?;
let results = if src == "-" {
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut std::io::stdin(), &mut buf)
.context("--decode-all: read stdin")?;
decode_all_bytes(&buf)?
} else {
decode_all_file(src)?
};
for d in &results {
println!("{}\t{}", d.format, d.text);
}
Ok(())
}
pub fn run(args: &Args) -> Result<()> {
let src = args
.decode
.as_ref()
.context("--decode requires an image path (or `-` for stdin)")?;
let hints: Vec<BarcodeFormat> = match args.decode_hints.as_deref() {
Some(s) if !s.trim().is_empty() => s
.split(',')
.map(|token| parse_format(token.trim()))
.collect::<Result<Vec<_>>>()?,
_ => Vec::new(),
};
let decoded = if src == "-" {
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut std::io::stdin(), &mut buf)
.context("--decode: read stdin")?;
decode_bytes(&buf, &hints)?
} else {
decode_file(src, &hints)?
};
println!("{}\t{}", decoded.format, decoded.text);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_format_accepts_curl_style() {
assert!(matches!(
parse_format("qr").unwrap(),
BarcodeFormat::QR_CODE
));
assert!(matches!(
parse_format("QR_CODE").unwrap(),
BarcodeFormat::QR_CODE
));
assert!(matches!(
parse_format("pdf417").unwrap(),
BarcodeFormat::PDF_417
));
assert!(matches!(
parse_format("aztec").unwrap(),
BarcodeFormat::AZTEC
));
assert!(matches!(
parse_format("ean13").unwrap(),
BarcodeFormat::EAN_13
));
assert!(parse_format("unknown-format").is_err());
}
#[test]
fn format_name_stable_for_common_types() {
assert_eq!(format_name(&BarcodeFormat::QR_CODE), "qr");
assert_eq!(format_name(&BarcodeFormat::DATA_MATRIX), "datamatrix");
assert_eq!(format_name(&BarcodeFormat::AZTEC), "aztec");
assert_eq!(format_name(&BarcodeFormat::PDF_417), "pdf417");
assert_eq!(format_name(&BarcodeFormat::CODE_128), "code128");
}
}