Skip to main content

browser_automation_cli/
qr_local.rs

1//! One-shot QR encode/decode (no Chrome, pure-Rust path).
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use image::Luma;
7use qrcode::QrCode;
8use serde_json::{json, Value};
9
10use crate::error::{CliError, ErrorKind};
11use crate::xdg;
12
13/// Encode text to PNG/SVG/terminal and optional path.
14pub fn encode(text: &str, format: &str, path: Option<&Path>) -> Result<Value, CliError> {
15    if text.is_empty() {
16        return Err(CliError::new(ErrorKind::Usage, "qr encode requires --text"));
17    }
18    let code = QrCode::new(text.as_bytes())
19        .map_err(|e| CliError::new(ErrorKind::Data, format!("qr encode: {e}")))?;
20    let fmt = format.trim().to_ascii_lowercase();
21    match fmt.as_str() {
22        "png" | "image" => {
23            let img = code.render::<Luma<u8>>().quiet_zone(true).build();
24            let out = resolve_out_path(path, "qr.png")?;
25            img.save(&out).map_err(|e| {
26                CliError::new(
27                    ErrorKind::Io,
28                    format!("write qr png {}: {e}", out.display()),
29                )
30            })?;
31            Ok(json!({
32                "action": "encode",
33                "format": "png",
34                "path": out.display().to_string(),
35                "bytes": text.len(),
36                "engine": "qrcode",
37            }))
38        }
39        "svg" => {
40            let svg = code
41                .render::<qrcode::render::svg::Color>()
42                .quiet_zone(true)
43                .build();
44            let out = resolve_out_path(path, "qr.svg")?;
45            fs::write(&out, svg.as_bytes()).map_err(|e| {
46                CliError::new(
47                    ErrorKind::Io,
48                    format!("write qr svg {}: {e}", out.display()),
49                )
50            })?;
51            Ok(json!({
52                "action": "encode",
53                "format": "svg",
54                "path": out.display().to_string(),
55                "engine": "qrcode",
56            }))
57        }
58        "terminal" | "text" | "unicode" => {
59            let s = code
60                .render::<char>()
61                .quiet_zone(true)
62                .module_dimensions(2, 1)
63                .build();
64            Ok(json!({
65                "action": "encode",
66                "format": "terminal",
67                "matrix": s,
68                "engine": "qrcode",
69            }))
70        }
71        other => Err(CliError::with_suggestion(
72            ErrorKind::Usage,
73            format!("unknown qr format: {other}"),
74            "Use png|svg|terminal",
75        )),
76    }
77}
78
79/// Decode QR payload from an image file.
80pub fn decode(path: &Path) -> Result<Value, CliError> {
81    let img = image::open(path).map_err(|e| {
82        CliError::new(
83            ErrorKind::Io,
84            format!("qr decode open {}: {e}", path.display()),
85        )
86    })?;
87    let luma = img.to_luma8();
88    let mut prepared = rqrr::PreparedImage::prepare(luma);
89    let grids = prepared.detect_grids();
90    if grids.is_empty() {
91        return Err(CliError::with_suggestion(
92            ErrorKind::Data,
93            "no QR code detected in image",
94            "Use a clear PNG/JPEG of a QR with quiet zone",
95        ));
96    }
97    let mut payloads = Vec::new();
98    for g in grids {
99        match g.decode() {
100            Ok((_meta, content)) => payloads.push(content),
101            Err(e) => payloads.push(format!("decode_error:{e}")),
102        }
103    }
104    Ok(json!({
105        "action": "decode",
106        "path": path.display().to_string(),
107        "count": payloads.len(),
108        "payloads": payloads,
109        "engine": "rqrr",
110    }))
111}
112
113fn resolve_out_path(path: Option<&Path>, default_name: &str) -> Result<PathBuf, CliError> {
114    if let Some(p) = path {
115        if let Some(parent) = p.parent() {
116            if !parent.as_os_str().is_empty() {
117                fs::create_dir_all(parent)
118                    .map_err(|e| CliError::new(ErrorKind::Io, format!("create qr dir: {e}")))?;
119            }
120        }
121        return Ok(p.to_path_buf());
122    }
123    let dir = xdg::cache_dir().unwrap_or_else(|_| PathBuf::from("."));
124    fs::create_dir_all(&dir).ok();
125    Ok(dir.join(default_name))
126}