use qrcode::render::unicode::Dense1x2;
use qrcode::{Color, QrCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QrStyle {
Braille,
Dense1x2,
}
pub fn render_login_qr(url: &str, style: QrStyle) -> Option<String> {
let code = QrCode::new(url.as_bytes()).ok()?;
let s = match style {
QrStyle::Braille => render_braille(&code),
QrStyle::Dense1x2 => code
.render::<Dense1x2>()
.dark_color(Dense1x2::Dark)
.light_color(Dense1x2::Light)
.quiet_zone(true)
.build(),
};
Some(s)
}
pub fn block_cols(rendered: &str) -> usize {
rendered
.lines()
.map(|l| l.chars().count())
.max()
.unwrap_or(0)
}
fn render_braille(code: &QrCode) -> String {
const BRAILLE_BITS: [[u32; 4]; 2] = [
[0x01, 0x02, 0x04, 0x40], [0x08, 0x10, 0x20, 0x80], ];
const QUIET: usize = 4;
let width = code.width();
let colors = code.to_colors();
let total = width + 2 * QUIET;
let is_dark = |x: usize, y: usize| -> bool {
if x < QUIET || x >= QUIET + width || y < QUIET || y >= QUIET + width {
return false;
}
matches!(colors[(x - QUIET) + (y - QUIET) * width], Color::Dark)
};
let cols = total.div_ceil(2);
let rows = total.div_ceil(4);
let mut out = String::with_capacity(rows * (cols * 3 + 1));
for cy in 0..rows {
for cx in 0..cols {
let mut bits: u32 = 0;
for dx in 0..2usize {
for dy in 0..4usize {
if is_dark(cx * 2 + dx, cy * 4 + dy) {
bits |= BRAILLE_BITS[dx][dy];
}
}
}
out.push(char::from_u32(0x2800 + bits).unwrap());
}
if cy + 1 < rows {
out.push('\n');
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_URL: &str =
"https://acs.atomgit.com/auth/login?state=abcd1234efgh5678ijkl9012mnop3456";
#[test]
fn braille_path_emits_braille_chars() {
let rendered = render_login_qr(SAMPLE_URL, QrStyle::Braille).expect("qr encodes");
assert!(
rendered
.chars()
.any(|c| (c as u32) >= 0x2800 && (c as u32) <= 0x28FF),
"expected at least one braille char in QR, got: {:?}",
rendered.chars().take(40).collect::<String>()
);
}
#[test]
fn dense1x2_path_emits_block_glyphs() {
let rendered = render_login_qr(SAMPLE_URL, QrStyle::Dense1x2).expect("qr encodes");
assert!(
rendered
.chars()
.any(|c| matches!(c, '\u{2588}' | '\u{2580}' | '\u{2584}')),
"expected at least one block glyph in dense1x2 QR, got: {:?}",
rendered.chars().take(40).collect::<String>()
);
}
#[test]
fn rendered_lines_have_uniform_width() {
for style in [QrStyle::Braille, QrStyle::Dense1x2] {
let rendered = render_login_qr(SAMPLE_URL, style).expect("qr encodes");
let widths: Vec<usize> = rendered.lines().map(|l| l.chars().count()).collect();
let first = widths[0];
assert!(
widths.iter().all(|w| *w == first),
"rendered QR ({:?}) has uneven line widths: {:?}",
style,
widths
);
}
}
#[test]
fn braille_is_about_half_size_of_dense1x2() {
let braille = render_login_qr(SAMPLE_URL, QrStyle::Braille).unwrap();
let dense = render_login_qr(SAMPLE_URL, QrStyle::Dense1x2).unwrap();
let (bw, bh) = (block_cols(&braille), braille.lines().count());
let (dw, dh) = (block_cols(&dense), dense.lines().count());
assert!(
bw * 2 <= dw + 1,
"braille width {} should be ~½ of dense1x2 width {}",
bw,
dw
);
assert!(
bh * 2 <= dh + 1,
"braille height {} should be ~½ of dense1x2 height {}",
bh,
dh
);
}
#[test]
fn block_cols_matches_widest_line() {
let rendered = "###\n#####\n##";
assert_eq!(block_cols(rendered), 5);
}
}