use qrcode::render::svg;
use qrcode::types::QrError;
use qrcode::{Color, EcLevel, QrCode};
const QUIET_ZONE: usize = 4;
const DARK_ON_WHITE: &str = "\x1b[30;107m";
const RESET: &str = "\x1b[0m";
fn encode(data: &str) -> Result<QrCode, QrError> {
QrCode::with_error_correction_level(data.as_bytes(), EcLevel::M)
}
pub fn terminal_lines(data: &str) -> Result<Vec<String>, QrError> {
let code = encode(data)?;
let width = code.width();
let colors = code.to_colors();
let side = width + 2 * QUIET_ZONE;
let dark = |x: usize, y: usize| -> bool {
if x < QUIET_ZONE || y < QUIET_ZONE {
return false;
}
let (cx, cy) = (x - QUIET_ZONE, y - QUIET_ZONE);
cx < width && cy < width && colors[cy * width + cx] == Color::Dark
};
let mut lines = Vec::with_capacity(side.div_ceil(2));
for y in (0..side).step_by(2) {
let mut line = String::from(DARK_ON_WHITE);
for x in 0..side {
line.push(match (dark(x, y), dark(x, y + 1)) {
(true, true) => '█',
(true, false) => '▀',
(false, true) => '▄',
(false, false) => ' ',
});
}
line.push_str(RESET);
lines.push(line);
}
Ok(lines)
}
pub fn svg(data: &str) -> Result<String, QrError> {
Ok(encode(data)?
.render::<svg::Color>()
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.quiet_zone(true)
.build())
}
#[cfg(test)]
mod tests {
use super::*;
const DID: &str =
"did:webvh:QmXi1PZD4NEvcvjfErAzVoCGtBFEv7dhXZQJHvcFY4U83F:webvh.storm.ws:first-vtc";
fn drawn_grid(lines: &[String]) -> Vec<Vec<bool>> {
let mut rows = Vec::new();
for line in lines {
let body = line
.strip_prefix(DARK_ON_WHITE)
.and_then(|l| l.strip_suffix(RESET))
.expect("every line paints its own colours and resets them");
let (mut top, mut bottom) = (Vec::new(), Vec::new());
for ch in body.chars() {
let (t, b) = match ch {
'█' => (true, true),
'▀' => (true, false),
'▄' => (false, true),
' ' => (false, false),
other => panic!("unexpected glyph {other:?}"),
};
top.push(t);
bottom.push(b);
}
rows.push(top);
rows.push(bottom);
}
rows
}
#[test]
fn the_terminal_draws_exactly_the_code_inside_a_quiet_zone() {
let code = encode(DID).unwrap();
let width = code.width();
let colors = code.to_colors();
let grid = drawn_grid(&terminal_lines(DID).unwrap());
let side = width + 2 * QUIET_ZONE;
for (y, row) in grid.iter().enumerate().take(side) {
assert_eq!(row.len(), side, "row {y} width");
for (x, &drawn) in row.iter().enumerate() {
let inside = (QUIET_ZONE..QUIET_ZONE + width).contains(&x)
&& (QUIET_ZONE..QUIET_ZONE + width).contains(&y);
let want =
inside && colors[(y - QUIET_ZONE) * width + (x - QUIET_ZONE)] == Color::Dark;
assert_eq!(drawn, want, "module ({x}, {y})");
}
}
assert!(grid.iter().skip(side).all(|row| row.iter().all(|d| !d)));
}
#[test]
fn a_did_webvh_is_a_version_5_code() {
assert_eq!(encode(DID).unwrap().width(), 37);
assert_eq!(terminal_lines(DID).unwrap().len(), 23);
}
#[test]
fn the_svg_is_standalone_and_scalable() {
let svg = svg(DID).unwrap();
assert!(svg.contains("<svg") && svg.contains("viewBox"), "{svg}");
assert!(!svg.contains("<script"));
}
}