use crate::types::qr::{QrBoundingBox, QrCode};
fn decode_qr_image(image_bytes: &[u8]) -> crate::Result<image::GrayImage> {
crate::extraction::image_decode::decode_standard_luma8_with_default_security_limits(image_bytes)
}
pub fn detect_qr_codes(image_bytes: &[u8], _format_hint: Option<&str>) -> Vec<QrCode> {
if image_bytes.is_empty() {
return Vec::new();
}
let luma = match decode_qr_image(image_bytes) {
Ok(img) => img,
Err(error) => {
tracing::warn!(error = %error, "qr: image decode failed; skipping");
return Vec::new();
}
};
let (width, height) = luma.dimensions();
let raw = luma.into_raw();
let mut prepared = rqrr::PreparedImage::prepare_from_greyscale(width as usize, height as usize, |x, y| {
raw[y * width as usize + x]
});
let grids = prepared.detect_grids();
if grids.is_empty() {
return Vec::new();
}
let mut results = Vec::with_capacity(grids.len());
for grid in grids {
let mut payload_bytes: Vec<u8> = Vec::new();
match grid.decode_to(&mut payload_bytes) {
Ok(_meta) => {
let payload = match String::from_utf8(payload_bytes) {
Ok(s) => s,
Err(err) => {
tracing::debug!(
error = %err,
"qr: non-UTF-8 payload, falling back to lossy decode"
);
String::from_utf8_lossy(err.as_bytes()).into_owned()
}
};
let bbox = bounding_box_from_corners(&grid.bounds);
results.push(QrCode {
payload,
confidence: Some(1.0),
bbox,
});
}
Err(error) => {
tracing::warn!(error = %error, "qr: grid decode failed; skipping grid");
}
}
}
results
}
fn bounding_box_from_corners(corners: &[rqrr::Point; 4]) -> Option<QrBoundingBox> {
let xs: [i32; 4] = [corners[0].x, corners[1].x, corners[2].x, corners[3].x];
let ys: [i32; 4] = [corners[0].y, corners[1].y, corners[2].y, corners[3].y];
let min_x = *xs.iter().min()?;
let max_x = *xs.iter().max()?;
let min_y = *ys.iter().min()?;
let max_y = *ys.iter().max()?;
let x = min_x.max(0) as u32;
let y = min_y.max(0) as u32;
let width = (max_x - min_x).max(0) as u32;
let height = (max_y - min_y).max(0) as u32;
Some(QrBoundingBox { x, y, width, height })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_bytes_returns_empty() {
assert!(detect_qr_codes(&[], None).is_empty());
}
#[test]
fn invalid_bytes_returns_empty() {
assert!(detect_qr_codes(&[0, 1, 2, 3, 4], Some("image/png")).is_empty());
}
#[test]
fn rejects_oversized_declared_dimensions_before_qr_decode() {
let bytes = crate::extraction::image_decode::bmp_with_declared_dimensions(6000, 6000);
let error = decode_qr_image(&bytes).expect_err("QR image decode must use the default security budget");
assert!(matches!(error, crate::XbergError::Validation { .. }));
assert!(error.to_string().contains("6000x6000"));
}
}