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