CGI bin for generating a QR code
1use image::codecs::avif::AvifEncoder;
2use image::codecs::png::PngEncoder;
3use image::{EncodableLayout, ExtendedColorType, ImageEncoder, Rgba};
4use qrcode::render::svg;
5use qrcode::{EcLevel, QrCode, Version};
6use serde::Deserialize;
7use serde_hex::{SerHex, SerHexOpt, StrictCap};
8use serde_querystring::ParseMode;
9use std::env;
10use std::io::{Write, stdout};
11
12#[derive(Copy, Clone, Deserialize, Debug, Default)]
13enum Format {
14 #[default]
15 Png,
16 Avif,
17 Svg,
18}
19
20#[derive(Copy, Clone, Deserialize, Debug, Default)]
21enum Ec {
22 L,
23 #[default]
24 M,
25 Q,
26 H,
27}
28
29impl From<Ec> for EcLevel {
30 fn from(ec: Ec) -> EcLevel {
31 match ec {
32 Ec::L => EcLevel::L,
33 Ec::M => EcLevel::M,
34 Ec::Q => EcLevel::Q,
35 Ec::H => EcLevel::H,
36 }
37 }
38}
39
40#[derive(Copy, Clone, Deserialize, Debug, Default)]
41enum Mode {
42 #[default]
43 Auto,
44 Standard,
45 Micro,
46}
47
48#[derive(Deserialize, Debug)]
49struct Options {
50 #[serde(default)]
51 width: Option<u32>,
52 #[serde(default, with = "SerHexOpt::<StrictCap>")]
53 fg: Option<[u8; 4]>,
54 #[serde(default, with = "SerHexOpt::<StrictCap>")]
55 bg: Option<[u8; 4]>,
56 #[serde(default)]
57 ec: Ec,
58 #[serde(default)]
59 version: Option<i16>,
60 #[serde(default)]
61 format: Format,
62 #[serde(default)]
63 mode: Mode,
64}
65
66fn generate_qr(data: &str) -> Result<(), String> {
67 let query = env::var("QUERY_STRING")
68 .expect("QUERY_STRING environment variable is expected; is this being called by CGI?");
69 let options: Options = serde_querystring::from_str(&query, ParseMode::UrlEncoded)
70 .map_err(|error| error.to_string())?;
71 let qrcode = match (
72 options.mode,
73 options.version.unwrap_or(9),
74 options.ec.into(),
75 ) {
76 (Mode::Auto, _, ec) => QrCode::with_error_correction_level(data.as_bytes(), ec)
77 .map_err(|error| error.to_string())?,
78 (Mode::Standard, ver, ec) => {
79 QrCode::with_version(data.as_bytes(), Version::Normal(ver), ec)
80 .map_err(|error| error.to_string())?
81 }
82 (Mode::Micro, ver, ec) => QrCode::with_version(data.as_bytes(), Version::Micro(ver), ec)
83 .map_err(|error| error.to_string())?,
84 };
85
86 let mut stdout = stdout().lock();
87 match options.format {
88 Format::Png => {
89 let mut renderer = qrcode.render::<Rgba<u8>>();
90 let width = u32::min(options.width.unwrap_or(256), 1024);
91 renderer.min_dimensions(width, width);
92 if let Some(fg) = options.fg {
93 renderer.dark_color(Rgba::<u8>(fg));
94 }
95 if let Some(bg) = options.bg {
96 renderer.light_color(Rgba::<u8>(bg));
97 }
98 writeln!(stdout, "Content-type: image/png").map_err(|error| error.to_string())?;
99 writeln!(stdout, "Content-disposition: attachment; filename=qr.png")
100 .map_err(|error| error.to_string())?;
101 writeln!(stdout).map_err(|error| error.to_string())?;
102 let encoder = PngEncoder::new(stdout);
103 let image = renderer.build();
104 encoder
105 .write_image(
106 image.as_bytes(),
107 image.width(),
108 image.height(),
109 ExtendedColorType::Rgba8,
110 )
111 .map_err(|error| error.to_string())?;
112 }
113 Format::Avif => {
114 let mut renderer = qrcode.render::<Rgba<u8>>();
115 let width = u32::min(options.width.unwrap_or(256), 1024);
116 renderer.min_dimensions(width, width);
117 if let Some(fg) = options.fg {
118 renderer.dark_color(Rgba::<u8>(fg));
119 }
120 if let Some(bg) = options.bg {
121 renderer.light_color(Rgba::<u8>(bg));
122 }
123 writeln!(stdout, "Content-type: image/avif").map_err(|error| error.to_string())?;
124 writeln!(stdout, "Content-disposition: attachment; filename=qr.avif")
125 .map_err(|error| error.to_string())?;
126 writeln!(stdout).map_err(|error| error.to_string())?;
127 let encoder = AvifEncoder::new(stdout);
128 let image = renderer.build();
129 encoder
130 .write_image(
131 image.as_bytes(),
132 image.width(),
133 image.height(),
134 ExtendedColorType::Rgba8,
135 )
136 .map_err(|error| error.to_string())?;
137 }
138 Format::Svg => {
139 let mut renderer = qrcode.render::<svg::Color>();
140 let width = u32::min(options.width.unwrap_or(256), 1024);
141 renderer.min_dimensions(width, width);
142 let fg = options.fg.map(|fg| {
143 let mut s = Vec::with_capacity(8);
144 SerHex::<StrictCap>::into_hex_raw(&fg, &mut s).unwrap();
145 format!("#{}", String::from_utf8(s).unwrap())
146 });
147 if let Some(fg) = &fg {
148 renderer.dark_color(svg::Color(fg));
149 }
150 let bg = options.bg.map(|bg| {
151 let mut s = Vec::with_capacity(8);
152 SerHex::<StrictCap>::into_hex_raw(&bg, &mut s).unwrap();
153 format!("#{}", String::from_utf8(s).unwrap())
154 });
155 if let Some(bg) = &bg {
156 renderer.light_color(svg::Color(bg));
157 }
158 writeln!(stdout, "Content-type: image/svg+xml").map_err(|error| error.to_string())?;
159 writeln!(stdout, "Content-disposition: attachment; filename=qr.svg")
160 .map_err(|error| error.to_string())?;
161 writeln!(stdout).map_err(|error| error.to_string())?;
162 write!(stdout, "{}", renderer.build()).map_err(|error| error.to_string())?;
163 }
164 }
165 Ok(())
166}
167
168fn main() {
169 let path = env::var("PATH_INFO")
170 .expect("PATH_INFO environment variable is expected; is this being called by CGI?");
171 if let Err(error) = generate_qr(&path[1..]) {
172 eprintln!("{error}");
173 println!("Content-type: text/plain\n");
174 println!("{error}");
175 }
176}