keyfork-zbar: initial commit

This commit is contained in:
Ryan Heywood 2024-01-13 02:52:43 -05:00
parent 9d7cbc17fa
commit 946b89349f
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
15 changed files with 391 additions and 27 deletions

20
Cargo.lock generated
View File

@ -1531,6 +1531,7 @@ dependencies = [
"keyfork-entropy", "keyfork-entropy",
"keyfork-mnemonic-util", "keyfork-mnemonic-util",
"keyfork-prompt", "keyfork-prompt",
"keyfork-qrcode",
"keyfork-shard", "keyfork-shard",
"keyforkd", "keyforkd",
"keyforkd-client", "keyforkd-client",
@ -1661,6 +1662,7 @@ name = "keyfork-qrcode"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"image", "image",
"keyfork-zbar",
"rqrr", "rqrr",
"thiserror", "thiserror",
"v4l", "v4l",
@ -1696,6 +1698,24 @@ dependencies = [
"hex", "hex",
] ]
[[package]]
name = "keyfork-zbar"
version = "0.1.0"
dependencies = [
"image",
"keyfork-zbar-sys",
"thiserror",
"v4l",
]
[[package]]
name = "keyfork-zbar-sys"
version = "0.1.0"
dependencies = [
"bindgen 0.68.1",
"pkg-config",
]
[[package]] [[package]]
name = "keyforkd" name = "keyforkd"
version = "0.1.0" version = "0.1.0"

View File

@ -16,6 +16,8 @@ members = [
"keyfork-shard", "keyfork-shard",
"keyfork-slip10-test-data", "keyfork-slip10-test-data",
"keyfork-qrcode", "keyfork-qrcode",
"keyfork-zbar",
"keyfork-zbar-sys",
"keyforkd", "keyforkd",
"keyforkd-client", "keyforkd-client",
"keyforkd-models", "keyforkd-models",

View File

@ -7,8 +7,14 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
decode-backend-rqrr = ["dep:rqrr"]
decode-backend-zbar = ["dep:keyfork-zbar"]
[dependencies] [dependencies]
image = { version = "0.24.7", default-features = false, features = ["jpeg"] } image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
rqrr = "0.6.0" keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true }
rqrr = { version = "0.6.0", optional = true }
thiserror = "1.0.56" thiserror = "1.0.56"
v4l = "0.14.0" v4l = "0.14.0"

View File

@ -1,5 +1,4 @@
use image::io::Reader as ImageReader; use image::io::Reader as ImageReader;
use rqrr::PreparedImage;
use std::{ use std::{
io::{Cursor, Write}, io::{Cursor, Write},
time::{Duration, SystemTime}, time::{Duration, SystemTime},
@ -8,13 +7,9 @@ use std::{
use v4l::{ use v4l::{
buffer::Type, buffer::Type,
io::{userptr::Stream, traits::CaptureStream}, io::{userptr::Stream, traits::CaptureStream},
video::Capture, Device,
Device, FourCC,
}; };
static MJPEG: &[u8; 4] = b"MJPG";
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum QRGenerationError { pub enum QRGenerationError {
#[error("{0}")] #[error("{0}")]
@ -37,9 +32,6 @@ pub enum QRCodeScanError {
#[error("Could not decode image: {0}")] #[error("Could not decode image: {0}")]
ImageDecode(#[from] image::ImageError), ImageDecode(#[from] image::ImageError),
#[error("Could not format FourCC as string (this is a bug!): {0}")]
FourCC(#[from] std::string::FromUtf8Error),
} }
#[derive(Default)] #[derive(Default)]
@ -81,24 +73,10 @@ pub fn qrencode(
Ok(result) Ok(result)
} }
#[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;
let mut format = device.format()?;
format.width = 1280;
format.height = 720;
format.fourcc = FourCC::new(MJPEG);
let format = device.set_format(&format)?;
if MJPEG != &format.fourcc.repr {
return Err(QRCodeScanError::CameraGaveBadFormat {
expected: String::from_utf8(MJPEG.to_vec())?,
actual: String::from_utf8(format.fourcc.repr.to_vec())?,
})
}
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?; let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = SystemTime::now(); let start = SystemTime::now();
while SystemTime::now() while SystemTime::now()
@ -111,7 +89,7 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
.with_guessed_format()? .with_guessed_format()?
.decode()? .decode()?
.to_luma8(); .to_luma8();
let mut image = PreparedImage::prepare(image); let mut image = rqrr::PreparedImage::prepare(image);
for grid in image.detect_grids() { for grid in image.detect_grids() {
if let Ok((_, content)) = grid.decode() { if let Ok((_, content)) = grid.decode() {
return Ok(Some(content)) return Ok(Some(content))
@ -121,3 +99,28 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
Ok(None) Ok(None)
} }
#[cfg(feature = "decode-backend-zbar")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = SystemTime::now();
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout
{
let (buffer, _) = stream.next()?;
let image = ImageReader::new(Cursor::new(buffer))
.with_guessed_format()?
.decode()?;
let image = keyfork_zbar::image::Image::from(image);
for symbol in scanner.scan_image(&image) {
return Ok(Some(String::from_utf8_lossy(symbol.data()).to_string()));
}
}
Ok(None)
}

View File

@ -0,0 +1,12 @@
[package]
name = "keyfork-zbar-sys"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[build-dependencies]
bindgen = { version = "0.68", default-features = false }
pkg-config = "0.3"

50
keyfork-zbar-sys/build.rs Normal file
View File

@ -0,0 +1,50 @@
use std::{env::VarError, path::Path};
use pkg_config::Config;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn env_var(var: &str) -> Result<String, VarError> {
println!("cargo:rerun-if-env-changed={var}");
std::env::var(var)
}
fn generate_bindings_file() -> Result<()> {
let zbar = Config::new().atleast_version("0.23.0").probe("zbar")?;
if env_var("ZBAR_STATIC").is_ok() {
println!("cargo:rustc-link-lib=static=zbar");
}
let mut builder = bindgen::builder()
.rustified_enum("zbar_color_e")
.rustified_non_exhaustive_enum("zbar_symbol_type_e")
.rustified_enum("zbar_orientation_e")
.rustified_non_exhaustive_enum("zbar_error_e")
.rustified_non_exhaustive_enum("zbar_config_e")
.rustified_enum("zbar_modifier_e")
.rustified_enum("video_control_type_e");
for path in zbar.include_paths {
builder = builder.clang_arg(format!("-I{}", path.display()));
}
let builder = builder.header("zbar-wrapper.h");
let bindings = builder.generate()?;
let out_path = Path::new(&env_var("OUT_DIR")?).join("bindings.rs");
bindings.write_to_file(out_path)?;
Ok(())
}
fn main() -> Result<()> {
if let Err(e) = generate_bindings_file() {
eprintln!("Building zbar-sys failed: {e}");
eprintln!("Ensure zbar headers, libclang, and pkg-config are installed");
return Err(e)
}
Ok(())
}

View File

@ -0,0 +1,3 @@
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

View File

@ -0,0 +1 @@
#include <zbar.h>

18
keyfork-zbar/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "keyfork-zbar"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["image"]
image = ["dep:image"]
[dependencies]
image = { version = "0.24.7", default-features = false, optional = true }
keyfork-zbar-sys = { version = "0.1.0", path = "../keyfork-zbar-sys" }
thiserror = "1.0.56"
[dev-dependencies]
v4l = "0.14.0"

View File

@ -0,0 +1,43 @@
use std::{
io::Cursor,
time::{Duration, SystemTime},
};
use keyfork_zbar::{image::Image, image_scanner::ImageScanner};
use image::io::Reader as ImageReader;
use v4l::{
buffer::Type,
io::{traits::CaptureStream, userptr::Stream},
Device,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let device = Device::new(0)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = SystemTime::now();
let mut scanner = ImageScanner::new();
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< Duration::from_secs(30)
{
let (buffer, _) = stream.next()?;
let image = Image::from(
ImageReader::new(Cursor::new(buffer))
.with_guessed_format()?
.decode()?,
);
for symbol in scanner.scan_image(&image) {
println!("{}", String::from_utf8_lossy(symbol.data()));
return Ok(());
}
}
println!("Could not find a QR code");
Ok(())
}

76
keyfork-zbar/src/image.rs Normal file
View File

@ -0,0 +1,76 @@
use super::sys;
pub struct Image {
pub(crate) inner: *mut sys::zbar_image_s,
/// Set to store the data of inner, as it will otherwise be freed when the data is dropped.
inner_data: Option<Vec<u8>>,
}
impl Image {
/// Link: [`sys::zbar_image_create`]
pub(crate) fn alloc() -> Self {
Self {
inner: unsafe { sys::zbar_image_create() },
inner_data: None,
}
}
/// Link: [`sys::zbar_image_set_format`]
///
/// A FourCC code can be given in the format:
///
/// ```no_run
/// self.set_format(b"Y800")
/// ````
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
let fourcc: u64 = fourcc[0] as u64
| ((fourcc[1] as u64) << 8)
| ((fourcc[2] as u64) << 16)
| ((fourcc[3] as u64) << 24);
unsafe { sys::zbar_image_set_format(self.inner, fourcc) }
}
/// Link: [`sys::zbar_image_set_size`]
fn set_size(&mut self, width: u32, height: u32) {
unsafe { sys::zbar_image_set_size(self.inner, width, height) }
}
/// Link: [`sys::zbar_image_set_data`]
///
/// Accepts raw data in the configured format. See: [`Image::set_format`]
fn set_data(&mut self, data: Vec<u8>) {
unsafe {
sys::zbar_image_set_data(
self.inner,
data.as_ptr().cast(),
data.len() as u64,
None,
)
}
// keep data in self to avoid use after free when data goes out of scope
let _ = self.inner_data.insert(data);
}
}
#[cfg(feature = "image")]
mod impls {
use super::*;
use image::{DynamicImage, GenericImageView};
impl From<DynamicImage> for Image {
fn from(value: DynamicImage) -> Self {
let mut image = Self::alloc();
let (width, height) = value.dimensions();
image.set_size(width, height);
image.set_format(b"Y800");
image.set_data(value.to_luma8().into_raw());
image
}
}
}
impl Drop for Image {
fn drop(&mut self) {
unsafe { sys::zbar_image_destroy(self.inner) }
}
}

View File

@ -0,0 +1,80 @@
use super::{
image::Image,
symbol::{Symbol, SymbolType},
sys, Config,
};
#[derive(thiserror::Error, Debug)]
pub enum ImageScannerError {
#[error("Unable to set Image Scanner configuration")]
UnableToSetConfig,
}
pub struct ImageScanner {
inner: *mut sys::zbar_image_scanner_t,
}
impl ImageScanner {
/// Link: [`sys::zbar_image_scanner_create`]
pub fn new() -> Self {
Self {
inner: unsafe { sys::zbar_image_scanner_create() },
}
}
/// Link: [`sys::zbar_image_scanner_set_config`]
pub fn set_config(
&mut self,
symbol: SymbolType,
config: Config,
value: i32,
) -> Result<(), ImageScannerError> {
let result =
unsafe { sys::zbar_image_scanner_set_config(self.inner, symbol, config, value) };
if result != 0 {
return Err(ImageScannerError::UnableToSetConfig);
}
Ok(())
}
/// Link: [`sys::zbar_scan_image`]
///
/// TODO: move `image` to newtype, offering conversions
/// to and from image::Image
///
/// TODO: return an iterator over scanned values
pub fn scan_image(
&mut self,
image: &Image,
) -> Vec<Symbol> {
unsafe { sys::zbar_scan_image(self.inner, image.inner) };
let mut result = vec![];
let mut symbol = unsafe { sys::zbar_image_first_symbol(image.inner) };
while !symbol.is_null() {
let symbol_type = unsafe { sys::zbar_symbol_get_type(symbol) };
let symbol_data = unsafe { sys::zbar_symbol_get_data(symbol) };
let symbol_data_len = unsafe { sys::zbar_symbol_get_data_length(symbol) };
let symbol_slice = unsafe {
std::slice::from_raw_parts(symbol_data as *const u8, symbol_data_len as usize)
};
result.push(Symbol::new(symbol_type, symbol_slice));
symbol = unsafe { sys::zbar_symbol_next(symbol) };
}
result
}
}
impl Default for ImageScanner {
fn default() -> Self {
Self::new()
}
}
impl Drop for ImageScanner {
fn drop(&mut self) {
unsafe { sys::zbar_image_scanner_destroy(self.inner) }
}
}

16
keyfork-zbar/src/lib.rs Normal file
View File

@ -0,0 +1,16 @@
//! Rustic bindings to the zbar library. Not intended to be high-level.
//!
//! This library includes a conversion from [`::image::DynamicImage`] for [`image::Image`].
//!
//! Reference: <https://github.com/mchehab/zbar>
pub use keyfork_zbar_sys as sys;
pub use sys::zbar_color_e as Color;
pub use sys::zbar_config_e as Config;
pub use sys::zbar_modifier_e as Modifier;
pub use sys::zbar_orientation_e as Orientation;
pub mod image_scanner;
pub mod image;
pub mod symbol;

View File

@ -0,0 +1,31 @@
use super::sys;
pub use sys::zbar_symbol_type_e as SymbolType;
// TODO: config, modifiers
#[derive(Debug)]
pub struct Symbol {
_type: SymbolType,
data: Vec<u8>,
}
impl Symbol {
pub(crate) fn new(_type: SymbolType, data: &[u8]) -> Self {
Self {
_type,
data: data.to_vec(),
}
}
pub fn _type(&self) -> SymbolType {
self._type
}
pub fn data(&self) -> &[u8] {
self.data.as_slice()
}
pub fn into_data(self) -> Vec<u8> {
self.data
}
}

View File

@ -5,8 +5,10 @@ edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
[features] [features]
default = ["completion"] default = ["completion", "qrcode-decode-backend-rqrr"]
completion = ["dep:clap_complete"] completion = ["dep:clap_complete"]
qrcode-decode-backend-rqrr = ["keyfork-qrcode/decode-backend-rqrr"]
qrcode-decode-backend-zbar = ["keyfork-qrcode/decode-backend-zbar"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -28,3 +30,4 @@ openpgp-card = "0.4.1"
keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt" } keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt" }
keyfork-entropy = { version = "0.1.0", path = "../keyfork-entropy" } keyfork-entropy = { version = "0.1.0", path = "../keyfork-entropy" }
clap_complete = { version = "4.4.6", optional = true } clap_complete = { version = "4.4.6", optional = true }
keyfork-qrcode = { version = "0.1.0", path = "../keyfork-qrcode" }