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    /// Returns [`QrError::DataTooLong`] if the input exceeds QR capacity.
56    pub fn new(data: &str) -> Result<Self, QrError> {
57        Self::with_ecl(data, Ecl::Medium)
58    }
59
60    /// Generate a QR code matrix with the specified error correction level.
61    ///
62    /// Returns [`QrError::DataTooLong`] if the input exceeds QR capacity
63    /// for the chosen level.
64    pub fn with_ecl(data: &str, ecl: Ecl) -> Result<Self, QrError> {
65        let qr = fast_qr::QRBuilder::new(data)
66            .ecl(ecl.to_fast_qr())
67            .build()
68            .map_err(|_| QrError::DataTooLong)?;
69        Ok(Self { qr })
70    }
71
72    /// Render the QR code as an SVG string.
73    ///
74    /// The SVG uses a `viewBox` (no fixed `width`/`height`) so it scales
75    /// to its container. Returns [`QrError::InvalidColor`] if any color
76    /// in `style` is malformed.
77    pub fn to_svg(&self, style: &QrStyle) -> Result<String, QrError> {
78        render::render_svg(&self.qr, style)
79    }
80
81    /// Returns the number of modules along one side of the QR matrix.
82    ///
83    /// This is the raw matrix dimension (e.g. 21 for Version 1) and does
84    /// not include the quiet zone added during SVG rendering.
85    pub fn size(&self) -> usize {
86        self.qr.size
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn new_creates_qrcode_from_url() {
96        let qr = QrCode::new("https://example.com").unwrap();
97        assert!(qr.size() > 0);
98    }
99
100    #[test]
101    fn new_creates_qrcode_from_otpauth_uri() {
102        let uri = "otpauth://totp/Example:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example";
103        let qr = QrCode::new(uri).unwrap();
104        assert!(qr.size() >= 21);
105    }
106
107    #[test]
108    fn new_creates_qrcode_from_empty_string() {
109        let qr = QrCode::new("");
110        assert!(qr.is_ok() || matches!(qr, Err(QrError::DataTooLong)));
111    }
112
113    #[test]
114    fn with_ecl_low() {
115        let qr = QrCode::with_ecl("test", Ecl::Low).unwrap();
116        assert!(qr.size() > 0);
117    }
118
119    #[test]
120    fn with_ecl_high() {
121        let qr = QrCode::with_ecl("test", Ecl::High).unwrap();
122        assert!(qr.size() > 0);
123    }
124
125    #[test]
126    fn higher_ecl_may_produce_larger_qr() {
127        let low = QrCode::with_ecl(
128            "Hello, World! This is some test data for QR codes.",
129            Ecl::Low,
130        )
131        .unwrap();
132        let high = QrCode::with_ecl(
133            "Hello, World! This is some test data for QR codes.",
134            Ecl::High,
135        )
136        .unwrap();
137        assert!(high.size() >= low.size());
138    }
139
140    #[test]
141    fn oversized_data_returns_data_too_long() {
142        let data = "x".repeat(8000);
143        let err = QrCode::new(&data).unwrap_err();
144        assert_eq!(err, QrError::DataTooLong);
145    }
146
147    #[test]
148    fn to_svg_produces_svg_string() {
149        let qr = QrCode::new("test").unwrap();
150        let svg = qr.to_svg(&QrStyle::default()).unwrap();
151        assert!(svg.starts_with("<svg"));
152        assert!(svg.contains("viewBox"));
153        assert!(svg.ends_with("</svg>"));
154    }
155
156    #[test]
157    fn size_returns_correct_dimension() {
158        let qr = QrCode::new("A").unwrap();
159        assert_eq!(qr.size(), 21);
160    }
161}