use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::{BitMatrix, Symbol, Symbology, SymbologyKind};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Ecc {
Low,
#[default]
Medium,
Quartile,
High,
}
impl Ecc {
fn to_upstream(self) -> ::qrcode::EcLevel {
match self {
Self::Low => ::qrcode::EcLevel::L,
Self::Medium => ::qrcode::EcLevel::M,
Self::Quartile => ::qrcode::EcLevel::Q,
Self::High => ::qrcode::EcLevel::H,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum QrVersion {
#[default]
Auto,
Fixed(u8),
}
const MIN_VERSION: u8 = 1;
const MAX_VERSION: u8 = 40;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Qr {
ecc: Ecc,
version: QrVersion,
}
impl Qr {
pub fn new() -> Self {
Self::default()
}
pub fn ecc(mut self, ecc: Ecc) -> Self {
self.ecc = ecc;
self
}
pub fn version(mut self, version: QrVersion) -> Self {
self.version = version;
self
}
pub fn ecc_level(&self) -> Ecc {
self.ecc
}
pub fn version_selection(&self) -> QrVersion {
self.version
}
}
impl Symbology for Qr {
fn kind(&self) -> SymbologyKind {
SymbologyKind::Qr
}
fn encode(&self, data: &str) -> Result<Symbol> {
if data.is_empty() {
return Err(Error::EmptyPayload);
}
let ec = self.ecc.to_upstream();
let code = match self.version {
QrVersion::Auto => ::qrcode::QrCode::with_error_correction_level(data, ec),
QrVersion::Fixed(v) => {
if !(MIN_VERSION..=MAX_VERSION).contains(&v) {
return Err(Error::Unencodable {
symbology: SymbologyKind::Qr.name(),
reason: alloc::format!(
"version must be between {MIN_VERSION} and {MAX_VERSION}, got {v}"
),
});
}
::qrcode::QrCode::with_version(data, ::qrcode::Version::Normal(i16::from(v)), ec)
}
}
.map_err(|e| Error::Unencodable {
symbology: SymbologyKind::Qr.name(),
reason: describe(&e),
})?;
let width = u32::try_from(code.width()).map_err(|_| Error::Unencodable {
symbology: SymbologyKind::Qr.name(),
reason: "symbol is implausibly large".to_string(),
})?;
let bits: Vec<bool> = code
.to_colors()
.into_iter()
.map(|c| c == ::qrcode::Color::Dark)
.collect();
let modules =
BitMatrix::from_vec(width, width, bits).ok_or_else(|| Error::Unencodable {
symbology: SymbologyKind::Qr.name(),
reason: "encoder returned a module count that is not a square".to_string(),
})?;
Ok(Symbol::new(SymbologyKind::Qr, modules, data.to_string()))
}
}
fn describe(e: &::qrcode::types::QrError) -> String {
use ::qrcode::types::QrError;
match e {
QrError::DataTooLong => {
"payload is too long for the chosen version and error correction level".to_string()
}
QrError::InvalidVersion => "invalid version and error correction combination".to_string(),
QrError::UnsupportedCharacterSet => {
"payload contains characters this QR mode cannot encode".to_string()
}
QrError::InvalidEciDesignator => "invalid ECI designator".to_string(),
QrError::InvalidCharacter => "payload contains an invalid character".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
#[test]
fn produces_a_square_symbol() {
let s = Qr::new().encode("PKG-9ED9285C").unwrap();
let m = s.modules();
assert_eq!(m.width(), m.height());
assert!(!s.is_linear());
assert_eq!(s.kind(), SymbologyKind::Qr);
assert_eq!(s.payload(), "PKG-9ED9285C");
}
#[test]
fn version_sizes_follow_the_standard() {
for v in [1u8, 2, 4, 10, 40] {
let s = Qr::new()
.version(QrVersion::Fixed(v))
.encode("PKG-9ED9285C")
.unwrap();
assert_eq!(
s.modules().width(),
17 + 4 * u32::from(v),
"version {v} has the wrong size"
);
}
}
#[test]
fn finder_patterns_sit_in_three_corners() {
let s = Qr::new().encode("PKG-9ED9285C").unwrap();
let m = s.modules();
let w = m.width();
let is_finder = |ox: u32, oy: u32| {
(0..7).all(|i| m.get(ox + i, oy) && m.get(ox + i, oy + 6))
&& (0..7).all(|i| m.get(ox, oy + i) && m.get(ox + 6, oy + i))
&& !m.get(ox + 1, oy + 1)
&& m.get(ox + 3, oy + 3)
};
assert!(is_finder(0, 0), "missing top-left finder");
assert!(is_finder(w - 7, 0), "missing top-right finder");
assert!(is_finder(0, w - 7), "missing bottom-left finder");
assert!(
!is_finder(w - 7, w - 7),
"bottom-right should hold format data, not a finder"
);
}
#[test]
fn higher_error_correction_needs_a_larger_symbol() {
let payload = "PKG-9ED9285C-0123456789-0123456789-0123456789";
let low = Qr::new().ecc(Ecc::Low).encode(payload).unwrap();
let high = Qr::new().ecc(Ecc::High).encode(payload).unwrap();
assert!(
high.modules().width() > low.modules().width(),
"High ECC ({}) should not fit in the same size as Low ({})",
high.modules().width(),
low.modules().width()
);
}
#[test]
fn auto_version_picks_a_small_symbol() {
let s = Qr::new().encode("PKG-9ED9285C").unwrap();
assert!(
s.modules().width() <= 25,
"12 characters should fit in version 2 or smaller, got {}",
s.modules().width()
);
}
#[test]
fn payload_too_long_for_a_fixed_version_is_an_error() {
let long = "X".repeat(2000);
let err = Qr::new()
.version(QrVersion::Fixed(1))
.encode(&long)
.unwrap_err();
assert!(matches!(err, Error::Unencodable { .. }), "got {err:?}");
assert!(format!("{err}").contains("too long"), "got {err}");
}
#[test]
fn out_of_range_versions_are_rejected() {
for v in [0u8, 41, 255] {
let err = Qr::new()
.version(QrVersion::Fixed(v))
.encode("PKG-9ED9285C")
.unwrap_err();
assert!(matches!(err, Error::Unencodable { .. }), "version {v}");
}
}
#[test]
fn rejects_an_empty_payload() {
assert!(matches!(Qr::new().encode(""), Err(Error::EmptyPayload)));
}
#[test]
fn encodes_unicode_payloads() {
let s = Qr::new().encode("PKG-\u{4e2d}\u{6587}").unwrap();
assert!(s.modules().width() >= 21);
}
#[test]
fn quiet_zone_is_four_modules() {
assert_eq!(SymbologyKind::Qr.required_quiet_zone(), 4);
assert!(!SymbologyKind::Qr.is_linear());
assert_eq!(format!("{}", SymbologyKind::Qr), "QR Code");
}
#[test]
fn encoding_is_deterministic() {
let a = Qr::new().encode("PKG-9ED9285C").unwrap();
let b = Qr::new().encode("PKG-9ED9285C").unwrap();
assert_eq!(a, b);
}
}