use crate::modules::{base, Command, Module};
use clap::{Arg, ArgMatches, SubCommand};
use image::Luma;
use qrcode::QrCode;
use std::io::{self, Cursor, Write};
pub fn module<'a, 'b>() -> Module<'a, 'b> {
Module {
desc: "QR Code generation".to_string(),
commands: commands(),
get_cases: cases::cases,
}
}
pub fn commands<'a, 'b>() -> Vec<Command<'a, 'b>> {
vec![Command {
app: SubCommand::with_name("s2qr")
.about("Convert string to QR code (PNG)")
.arg(Arg::with_name("INPUT").required(false).index(1)),
f: s2qr,
},
Command {
app: SubCommand::with_name("qr2s")
.about("Convert QR code image to string")
.arg(Arg::with_name("INPUT").required(false).index(1)), f: qr2s,
}]
}
fn s2qr(matches: &ArgMatches) -> Result<Vec<String>, String> {
let input = base::input_string(matches)?;
let code = QrCode::new(input.as_bytes()).map_err(|e| format!("Failed to generate QR code: {}", e))?;
let image = code.render::<Luma<u8>>().build();
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
image
.write_to(&mut cursor, image::ImageFormat::Png)
.map_err(|e| format!("Failed to write image: {}", e))?;
io::stdout()
.write_all(&buffer)
.map_err(|e| format!("Failed to write to stdout: {}", e))?;
Ok(vec![])
}
fn qr2s(_matches: &ArgMatches) -> Result<Vec<String>, String> {
let runtime = tokio::runtime::Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?;
runtime.block_on(async {
run_qr_scanner().await
})
}
async fn run_qr_scanner() -> Result<Vec<String>, String> {
use axum::{
routing::{get, post},
Router,
response::Html,
Json,
};
use std::net::TcpListener;
use tokio::sync::oneshot;
let (tx, rx) = oneshot::channel::<String>();
let tx = std::sync::Arc::new(std::sync::Mutex::new(Some(tx)));
let html = r#"
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QR Code Scanner</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: #f0f0f0;
}
#video-container {
position: relative;
width: 90vw;
max-width: 500px;
aspect-ratio: 1;
background: #000;
border-radius: 10px;
overflow: hidden;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
#result {
margin-top: 20px;
padding: 15px;
background: white;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 500px;
word-break: break-all;
}
.success {
color: green;
font-weight: bold;
}
h1 {
color: #333;
}
</style>
</head>
<body>
<h1>QR Code Scanner</h1>
<div id="video-container">
<video id="video"></video>
</div>
<div id="result">Waiting for QR code...</div>
<script type="module">
import QrScanner from 'https://cdn.jsdelivr.net/npm/qr-scanner@1.4.2/qr-scanner.min.js';
const video = document.getElementById('video');
const resultDiv = document.getElementById('result');
const qrScanner = new QrScanner(
video,
result => {
resultDiv.innerHTML = '<span class="success">QR Code detected!</span><br>' + result.data;
qrScanner.stop();
// Send result to server
fetch('/result', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: result.data })
}).then(() => {
setTimeout(() => {
resultDiv.innerHTML += '<br><br>You can close this window now.';
}, 500);
});
},
{
returnDetailedScanResult: true,
highlightScanRegion: true,
highlightCodeOutline: true,
}
);
qrScanner.start().catch(err => {
resultDiv.textContent = 'Error: ' + err;
});
</script>
</body>
</html>
"#;
let html_clone = html.to_string();
let tx_clone = tx.clone();
let app = Router::new()
.route("/", get(move || async move { Html(html_clone) }))
.route("/result", post(move |Json(payload): Json<serde_json::Value>| async move {
if let Some(data) = payload.get("data").and_then(|v| v.as_str()) {
if let Some(sender) = tx_clone.lock().unwrap().take() {
let _ = sender.send(data.to_string());
}
}
"OK"
}));
let listener = TcpListener::bind("127.0.0.1:0")
.map_err(|e| format!("Failed to bind to port: {}", e))?;
let addr = listener.local_addr()
.map_err(|e| format!("Failed to get local address: {}", e))?;
let url = format!("http://{}", addr);
eprintln!("QR Scanner running at: {}", url);
if let Err(e) = open_browser(&url) {
eprintln!("Failed to open browser: {}. Please open {} manually.", e, url);
}
listener.set_nonblocking(true)
.map_err(|e| format!("Failed to set non-blocking: {}", e))?;
let listener = tokio::net::TcpListener::from_std(listener)
.map_err(|e| format!("Failed to convert listener: {}", e))?;
let server = axum::serve(listener, app);
let server_handle = tokio::spawn(async move {
server.await
});
let result = rx.await
.map_err(|_| "Failed to receive QR code result".to_string())?;
server_handle.abort();
Ok(vec![result])
}
fn open_browser(url: &str) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
std::process::Command::new("open")
.arg(url)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(target_os = "linux")]
{
std::process::Command::new("xdg-open")
.arg(url)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(target_os = "windows")]
{
std::process::Command::new("cmd")
.args(&["/C", "start", url])
.spawn()
.map_err(|e| e.to_string())?;
}
Ok(())
}
mod cases {
use crate::modules::Case;
use linked_hash_map::LinkedHashMap;
pub fn cases() -> LinkedHashMap<&'static str, Vec<Case>> {
vec![
(
"s2qr",
vec![Case {
desc: "Generate QR code for 'hello'".to_string(),
input: vec!["hello".to_string()],
output: vec![],
is_example: true,
is_test: false, since: "0.15.0".to_string(),
}],
),
(
"qr2s",
vec![Case {
desc: "Scan QR code from camera (interactive)".to_string(),
input: vec![],
output: vec![],
is_example: true,
is_test: false, since: "0.15.0".to_string(),
}],
),
]
.into_iter()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modules::base::test::test_module;
#[test]
fn test_cases() {
test_module(module());
}
}