CGI bin for generating a QR code
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 176 lines 6.2 kB view raw
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}