//! Encoding and decoding QR codes. use keyfork_bug as bug; use image::{ImageBuffer, ImageReader, Luma}; use std::{ io::{Cursor, Write}, process::{Command, Stdio}, time::{Duration, Instant}, }; use v4l::{ buffer::Type, io::{traits::CaptureStream, userptr::Stream}, video::Capture, Device, FourCC, }; /// A QR code could not be generated. #[derive(thiserror::Error, Debug)] pub enum QRGenerationError { /// The resulting QR coode could not be read from the generator program. #[error("{0}")] Io(#[from] std::io::Error), /// The generator program produced invalid data. #[error("Could not decode output of qrencode (this is a bug!): {0}")] StringParse(#[from] std::string::FromUtf8Error), } /// An error occurred while scanning for a QR code. #[derive(thiserror::Error, Debug)] pub enum QRCodeScanError { /// The camera could not load the requested format. #[error("Camera could not use {expected} format, instead used {actual}")] CameraGaveBadFormat { /// The expected format, in FourCC format. expected: String, /// The actual format, in FourCC format. actual: String, }, /// Interfacing with the camera resulted in an error. #[error("Unable to interface with camera: {0}")] CameraIO(#[from] std::io::Error), /// Decoding an image from the camera resulted in an error. #[error("Could not decode image: {0}")] ImageDecode(#[from] image::ImageError), } /// The level of error correction when generating a QR code. #[derive(Default)] pub enum ErrorCorrection { /// 7% of the QR code can be recovered. #[default] Lowest, /// 15% of the QR code can be recovered. Medium, /// 25% of the QR code can be recovered. Quartile, /// 30% of the QR code can be recovered. Highest, } /// Generate a terminal-printable QR code for a given string. Uses the `qrencode` CLI utility. /// /// # Errors /// The function may return an error if interacting with the QR code generation program fails. pub fn qrencode( text: &str, error_correction: impl Into>, ) -> Result { let error_correction_arg = match error_correction.into().unwrap_or_default() { ErrorCorrection::Lowest => "L", ErrorCorrection::Medium => "M", ErrorCorrection::Quartile => "Q", ErrorCorrection::Highest => "H", }; let mut qrencode = Command::new("qrencode") .arg("-t") .arg("ansiutf8") .arg("-m") .arg("2") .arg("-l") .arg(error_correction_arg) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; if let Some(stdin) = qrencode.stdin.as_mut() { stdin.write_all(text.as_bytes())?; } let output = qrencode.wait_with_output()?; let result = String::from_utf8(output.stdout)?; Ok(result) } const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format"; trait Scanner { fn scan_image(&mut self, image: ImageBuffer, Vec>) -> Option; } #[cfg(feature = "decode-backend-zbar")] mod zbar { use super::{ImageBuffer, Luma, Scanner}; pub struct Zbar { scanner: keyfork_zbar::image_scanner::ImageScanner, } impl Zbar { #[allow(dead_code)] pub fn new() -> Self { Self::default() } } impl Default for Zbar { fn default() -> Self { Self { scanner: keyfork_zbar::image_scanner::ImageScanner::new(), } } } impl Scanner for Zbar { fn scan_image( &mut self, image: ImageBuffer, Vec>, ) -> Option { let image = keyfork_zbar::image::Image::from(image); self.scanner.scan_image(&image).into_iter().next().map(|symbol| { String::from_utf8_lossy(symbol.data()).into() }) } } } #[cfg(feature = "decode-backend-rqrr")] mod rqrr { use super::{ImageBuffer, Luma, Scanner}; pub struct Rqrr; impl Scanner for Rqrr { fn scan_image( &mut self, image: ImageBuffer, Vec>, ) -> Option { let mut image = rqrr::PreparedImage::prepare(image); for grid in image.detect_grids() { if let Ok((_, content)) = grid.decode() { return Some(content); } } None } } } #[allow(dead_code)] fn dbg_elapsed(count: u64, instant: Instant) { let elapsed = instant.elapsed().as_secs(); let framerate = count as f64 / elapsed as f64; eprintln!("framerate: {count}/{elapsed} = {framerate}"); } /// Continuously scan the `index`-th camera for a QR code. /// /// # Errors /// /// The function may return an error if the hardware is unable to scan video or if an image could /// not be decoded. pub fn scan_camera(timeout: Duration, index: usize) -> Result, QRCodeScanError> { let device = Device::new(index)?; let mut fmt = device .format() .unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR)); fmt.fourcc = FourCC::new(b"MPG1"); device.set_format(&fmt)?; let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?; let start = Instant::now(); cfg_if::cfg_if! { if #[cfg(feature = "decode-backend-zbar")] { let mut scanner = zbar::Zbar::default(); } else if #[cfg(feature = "decode-backend-rqrr")] { let mut scanner = rqrr::Rqrr; } else { unimplemented!("neither decode-backend-zbar nor decode-backend-rqrr were selected") } }; #[allow(unused)] let mut count = 0; while Instant::now().duration_since(start) < timeout { count += 1; let (buffer, _) = stream.next()?; let image = ImageReader::new(Cursor::new(buffer)) .with_guessed_format()? .decode()? .to_luma8(); if let Some(content) = scanner.scan_image(image) { // dbg_elapsed(count, start); return Ok(Some(content)); } } // dbg_elapsed(count, start); Ok(None) }