Skip to main content

modo/qrcode/
code.rs

1use crate::qrcode::error::QrError;
2use crate::qrcode::render;
3use crate::qrcode::style::QrStyle;
4
5/// Error correction level for QR code generation.
6///
7/// Higher levels increase data recovery at the cost of larger QR codes.
8/// [`QrCode::new`] defaults to [`Ecl::Medium`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Ecl {
11    /// Low — recovers ~7% of data.
12    Low,
13    /// Medium — recovers ~15% of data. This is the default.
14    Medium,
15    /// Quartile — recovers ~25% of data.
16    Quartile,
17    /// High — recovers ~30% of data.
18    High,
19}
20
21impl Ecl {
22    fn to_fast_qr(self) -> fast_qr::ECL {
23        match self {
24            Self::Low => fast_qr::ECL::L,
25            Self::Medium => fast_qr::ECL::M,
26            Self::Quartile => fast_qr::ECL::Q,
27            Self::High => fast_qr::ECL::H,
28        }
29    }
30}
31
32/// A generated QR code ready for SVG rendering.
33///
34/// Create with [`QrCode::new`] (default error correction) or
35/// [`QrCode::with_ecl`] (explicit level), then render via
36/// [`QrCode::to_svg`].
37///
38/// # Example
39///
40/// ```
41/// use modo::qrcode::{QrCode, QrStyle, Ecl};
42///
43/// let qr = QrCode::with_ecl("https://example.com", Ecl::High).unwrap();
44/// let svg = qr.to_svg(&QrStyle::default()).unwrap();
45/// assert!(svg.starts_with("<svg"));
46/// ```
47#[derive(Debug)]
48pub struct QrCode {
49    pub(super) qr: fast_qr::QRCode,
50}
51
52impl QrCode {
53    /// Generate a QR code matrix with default error correction ([`Ecl::Medium`]).
54    ///
55    /// # Errors
56    ///
57    /// Returns [`QrError::DataTooLong`] if the input exceeds QR capacity.
58    pub fn new(data: &str) -> Result<Self, QrError> {
59        Self::with_ecl(data, Ecl::Medium)
60    }
61
62    /// Generate a QR code matrix with the specified error correction level.
63    ///
64    /// # Errors
65    ///
66    /// Returns [`QrError::DataTooLong`] if the input exceeds QR capacity
67    /// for the chosen level.
68    pub fn with_ecl(data: &str, ecl: Ecl) -> Result<Self, QrError> {
69        let qr = fast_qr::QRBuilder::new(data)
70            .ecl(ecl.to_fast_qr())
71            .build()
72            .map_err(|_| QrError::DataTooLong)?;
73        Ok(Self { qr })
74    }
75
76    /// Render the QR code as an SVG string.
77    ///
78    /// The SVG uses a `viewBox` (no fixed `width`/`height`) so it scales
79    /// to its container.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`QrError::InvalidColor`] if any color in `style` is malformed.
84    pub fn to_svg(&self, style: &QrStyle) -> Result<String, QrError> {
85        render::render_svg(&self.qr, style)
86    }
87
88    /// Returns the number of modules along one side of the QR matrix.
89    ///
90    /// This is the raw matrix dimension (e.g. 21 for Version 1) and does
91    /// not include the quiet zone added during SVG rendering.
92    pub fn size(&self) -> usize {
93        self.qr.size
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn new_creates_qrcode_from_url() {
103        let qr = QrCode::new("https://example.com").unwrap();
104        assert!(qr.size() > 0);
105    }
106
107    #[test]
108    fn new_creates_qrcode_from_otpauth_uri() {
109        let uri = "otpauth://totp/Example:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example";
110        let qr = QrCode::new(uri).unwrap();
111        assert!(qr.size() >= 21);
112    }
113
114    #[test]
115    fn new_creates_qrcode_from_empty_string() {
116        let qr = QrCode::new("");
117        assert!(qr.is_ok() || matches!(qr, Err(QrError::DataTooLong)));
118    }
119
120    #[test]
121    fn with_ecl_low() {
122        let qr = QrCode::with_ecl("test", Ecl::Low).unwrap();
123        assert!(qr.size() > 0);
124    }
125
126    #[test]
127    fn with_ecl_high() {
128        let qr = QrCode::with_ecl("test", Ecl::High).unwrap();
129        assert!(qr.size() > 0);
130    }
131
132    #[test]
133    fn higher_ecl_may_produce_larger_qr() {
134        let low = QrCode::with_ecl(
135            "Hello, World! This is some test data for QR codes.",
136            Ecl::Low,
137        )
138        .unwrap();
139        let high = QrCode::with_ecl(
140            "Hello, World! This is some test data for QR codes.",
141            Ecl::High,
142        )
143        .unwrap();
144        assert!(high.size() >= low.size());
145    }
146
147    #[test]
148    fn oversized_data_returns_data_too_long() {
149        let data = "x".repeat(8000);
150        let err = QrCode::new(&data).unwrap_err();
151        assert_eq!(err, QrError::DataTooLong);
152    }
153
154    #[test]
155    fn to_svg_produces_svg_string() {
156        let qr = QrCode::new("test").unwrap();
157        let svg = qr.to_svg(&QrStyle::default()).unwrap();
158        assert!(svg.starts_with("<svg"));
159        assert!(svg.contains("viewBox"));
160        assert!(svg.ends_with("</svg>"));
161    }
162
163    #[test]
164    fn size_returns_correct_dimension() {
165        let qr = QrCode::new("A").unwrap();
166        assert_eq!(qr.size(), 21);
167    }
168}