use std::fmt;
use rich::measure::Measurement;
use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
use crate::chart::{Charset, has_colour, user_style};
const SPEC_QUIET_ZONE: usize = 4;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ErrorCorrection {
Low,
#[default]
Medium,
Quartile,
High,
}
impl ErrorCorrection {
fn ecc(self) -> qrcodegen::QrCodeEcc {
match self {
ErrorCorrection::Low => qrcodegen::QrCodeEcc::Low,
ErrorCorrection::Medium => qrcodegen::QrCodeEcc::Medium,
ErrorCorrection::Quartile => qrcodegen::QrCodeEcc::Quartile,
ErrorCorrection::High => qrcodegen::QrCodeEcc::High,
}
}
fn from_ecc(ecc: qrcodegen::QrCodeEcc) -> Self {
match ecc {
qrcodegen::QrCodeEcc::Low => ErrorCorrection::Low,
qrcodegen::QrCodeEcc::Medium => ErrorCorrection::Medium,
qrcodegen::QrCodeEcc::Quartile => ErrorCorrection::Quartile,
qrcodegen::QrCodeEcc::High => ErrorCorrection::High,
}
}
pub fn word(self) -> &'static str {
match self {
ErrorCorrection::Low => "low",
ErrorCorrection::Medium => "medium",
ErrorCorrection::Quartile => "quartile",
ErrorCorrection::High => "high",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum QrError {
TooLong {
bytes: usize,
},
}
impl fmt::Display for QrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QrError::TooLong { bytes } => write!(
f,
"{bytes} bytes do not fit a QR code at this error correction"
),
}
}
}
impl std::error::Error for QrError {}
#[derive(Clone, Debug, PartialEq)]
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct QrCode {
modules: Vec<bool>,
size: usize,
version: u8,
ecc: ErrorCorrection,
text: Option<String>,
quiet: usize,
charset: Charset,
invert: bool,
dark: String,
light: String,
}
impl QrCode {
pub fn new(text: &str) -> Result<Self, QrError> {
Self::with_error_correction(text, ErrorCorrection::Medium)
}
pub fn with_error_correction(text: &str, ecc: ErrorCorrection) -> Result<Self, QrError> {
let code = qrcodegen::QrCode::encode_text(text, ecc.ecc())
.map_err(|_| QrError::TooLong { bytes: text.len() })?;
Ok(Self::from_code(&code, Some(text.to_string())))
}
pub fn from_bytes(data: &[u8], ecc: ErrorCorrection) -> Result<Self, QrError> {
let code = qrcodegen::QrCode::encode_binary(data, ecc.ecc())
.map_err(|_| QrError::TooLong { bytes: data.len() })?;
let text = std::str::from_utf8(data).ok().map(str::to_string);
Ok(Self::from_code(&code, text))
}
fn from_code(code: &qrcodegen::QrCode, text: Option<String>) -> Self {
let size = usize::try_from(code.size()).unwrap_or(0);
let mut modules = Vec::with_capacity(size * size);
for y in 0..code.size() {
for x in 0..code.size() {
modules.push(code.get_module(x, y));
}
}
QrCode {
modules,
size,
version: code.version().value(),
ecc: ErrorCorrection::from_ecc(code.error_correction_level()),
text,
quiet: SPEC_QUIET_ZONE,
charset: Charset::Auto,
invert: false,
dark: "#000000".to_string(),
light: "#ffffff".to_string(),
}
}
pub fn size(&self) -> usize {
self.size
}
pub fn version(&self) -> u8 {
self.version
}
pub fn error_correction(&self) -> ErrorCorrection {
self.ecc
}
pub fn module(&self, x: usize, y: usize) -> bool {
x < self.size && y < self.size && self.modules[y * self.size + x]
}
pub fn quiet_zone(mut self, modules: usize) -> Self {
self.quiet = modules.min(SPEC_QUIET_ZONE * 4);
self
}
pub fn charset(mut self, charset: Charset) -> Self {
self.charset = charset;
self
}
pub fn invert(mut self, invert: bool) -> Self {
self.invert = invert;
self
}
pub fn colours(mut self, dark: impl Into<String>, light: impl Into<String>) -> Self {
self.dark = dark.into();
self.light = light.into();
self
}
fn dark_at(&self, quiet: usize, x: usize, y: usize) -> bool {
x >= quiet && y >= quiet && self.module(x - quiet, y - quiet)
}
fn width_for(&self, quiet: usize, ascii: bool) -> usize {
(self.size + 2 * quiet) * if ascii { 2 } else { 1 }
}
fn fit(&self, width: usize, ascii: bool) -> Option<usize> {
(self.quiet.min(1)..=self.quiet)
.rev()
.find(|&q| self.width_for(q, ascii) <= width)
}
fn note(&self, ascii: bool) -> String {
format!(
"QR code needs {} columns",
self.width_for(self.quiet.min(1), ascii)
)
}
fn colour_styles(&self, console: &Console) -> (Style, Style, Style) {
let colour_of = |spec: &str| user_style(console, spec).color().cloned();
let (dark, light) = (colour_of(&self.dark), colour_of(&self.light));
(
Style::from_color(dark.clone(), light.clone()),
Style::from_color(None, dark),
Style::from_color(None, light),
)
}
fn lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let charset = self.charset.resolve(console, options, Charset::Blocks);
let ascii = charset == Charset::Ascii;
let Some(quiet) = self.fit(options.max_width, ascii) else {
let note = crate::chart::truncate(&self.note(ascii), options.max_width, ascii);
return vec![Segment::new(note, None)];
};
let colour = has_colour(console);
let (ink, dark_bg, light_bg) = self.colour_styles(console);
let side = self.size + 2 * quiet;
let dark = |x: usize, y: usize| self.dark_at(quiet, x, y);
let inked = |x: usize, y: usize| {
y < side
&& if colour || self.invert {
dark(x, y)
} else {
!dark(x, y)
}
};
let mut out = Vec::new();
for y in 0..side {
if ascii {
if y > 0 {
out.push(Segment::line());
}
let mut x = 0;
while x < side {
let here = if colour { dark(x, y) } else { inked(x, y) };
let run = (x..side)
.take_while(|&i| (if colour { dark(i, y) } else { inked(i, y) }) == here)
.count();
let segment = if colour {
let bg = if here { &dark_bg } else { &light_bg };
Segment::new(" ".repeat(run * 2), Some(bg.clone()))
} else {
Segment::new(if here { "##" } else { " " }.repeat(run), None)
};
out.push(segment);
x += run;
}
} else if y % 2 == 0 {
if y > 0 {
out.push(Segment::line());
}
let row: String = (0..side)
.map(|x| match (inked(x, y), inked(x, y + 1)) {
(true, true) => '█',
(true, false) => '▀',
(false, true) => '▄',
(false, false) => ' ',
})
.collect();
out.push(Segment::new(row, colour.then(|| ink.clone())));
}
}
out
}
}
impl Renderable for QrCode {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.lines(console, options)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
let ascii = self.charset.resolve(console, options, Charset::Blocks) == Charset::Ascii;
let natural = self.width_for(self.quiet, ascii);
let least = self.width_for(self.quiet.min(1), ascii);
Measurement::new(least, natural)
.with_maximum(options.max_width)
.normalize()
}
}
impl crate::a11y::AccessibleText for QrCode {
fn accessible_text(&self, _width: usize) -> String {
let mut out = format!(
"QR code, version {}, {} by {} modules, {} error correction",
self.version,
self.size,
self.size,
self.ecc.word()
);
match &self.text {
Some(text) => out.push_str(&format!("\nEncodes: {text}")),
None => out.push_str("\nEncodes binary data"),
}
out
}
}