//! Encoding and decoding QR codes. #![allow(clippy::expect_fun_call)] use keyfork_bug as bug; use bug::POISONED_MUTEX; use image::{ImageBuffer, ImageReader, Luma}; use std::{ io::{Cursor, Write}, process::{Command, Stdio}, sync::{mpsc::channel, Arc, Condvar, Mutex}, time::{Duration, Instant}, }; use v4l::{ buffer::Type, io::{traits::CaptureStream, userptr::Stream}, video::Capture, Device, FourCC, }; type Image = ImageBuffer, Vec>; /// 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: Image) -> Option; } #[cfg(feature = "decode-backend-zbar")] mod zbar { use super::{Image, 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: Image) -> 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::{Image, Scanner}; pub struct Rqrr; impl Scanner for Rqrr { fn scan_image(&mut self, image: Image) -> 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}"); std::thread::sleep(std::time::Duration::from_secs(5)); } #[derive(Debug)] struct ScanQueue { shutdown: bool, images: Vec, } /// 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. /// /// # Panics /// /// The function may panic if a mutex is poisoned by a thread panicking, which should /// only happen during a mutex, or if it can't send a message over the mpsc channel. 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(); #[allow(unused)] let mut count = 0; let thread_count = 4; std::thread::scope(|scope| { let scan_queue = ScanQueue { shutdown: false, images: vec![], }; let arced = Arc::new((Mutex::new(scan_queue), Condvar::new())); let (tx, rx) = channel(); for _ in 0..thread_count { let tx = tx.clone(); let arced = arced.clone(); scope.spawn(move || { 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") } }; let (queue_mutex, condvar) = &*arced; loop { // NOTE: Carrying the `queue` variable through the loop, so we can // pass it through without re-locking, means that we don't drop the // lock on the mutex. Therefore, we unlock, then immediately // re-lock when we pass the value to wait_while(). // // By holding onto the queue until we pass it back to the Condvar, // and checking shutdown, we ensure that there's no way we miss the // shutdown being set before we release the guard on the queue. let queue = queue_mutex.lock().expect(bug::bug!(POISONED_MUTEX)); if queue.shutdown { break; } let mut queue = condvar .wait_while(queue, |queue| { queue.images.is_empty() && !queue.shutdown }) .expect(bug::bug!(POISONED_MUTEX)); if let Some(image) = queue.images.pop() { // drop the queue here since this is what's going to take time drop(queue); if let Some(content) = scanner.scan_image(image) { if tx.send(content).is_err() { break; } } } } }); } while Instant::now().duration_since(start) < timeout { if let Ok(content) = rx.try_recv() { arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true; arced.1.notify_all(); return Ok(Some(content)); } count += 1; let (buffer, _) = stream.next()?; let image = ImageReader::new(Cursor::new(buffer)) .with_guessed_format()? .decode()? .to_luma8(); arced .0 .lock() .expect(bug::bug!(POISONED_MUTEX)) .images .push(image); arced.1.notify_one(); } // dbg_elapsed(count, start); arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true; arced.1.notify_all(); Ok(None) }) }