browser_automation_cli/
qr_local.rs1use 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
13pub 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
79pub 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}