Skip to main content

roka_qr/
lib.rs

1#![deny(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3
4//! Zero-dependency QR code encoder + decoder with built-in PNG/PBM image I/O.
5//!
6//! `roka-qr` covers [ISO/IEC 18004] from end to end: byte / alphanumeric /
7//! numeric mode encoding, all four error-correction levels (L/M/Q/H), all 40
8//! versions, and decoding back from a PNG or PBM image without ever pulling in
9//! an external crate.
10//!
11//! # Quick start
12//!
13//! Encode a string into a QR code and write it as PNG:
14//!
15//! ```
16//! use roka_qr::{Encoder, EcLevel};
17//!
18//! let code = Encoder::new(b"https://example.com").ec_level(EcLevel::M).build()?;
19//! let bitmap = code.render().scale(8).quiet_zone(4).build();
20//! let png_bytes = bitmap.to_png();
21//! # Ok::<(), roka_qr::Error>(())
22//! ```
23//!
24//! Decode a QR code from PNG bytes:
25//!
26//! ```no_run
27//! use roka_qr::Reader;
28//!
29//! let png_bytes: Vec<u8> = std::fs::read("qr.png").unwrap();
30//! let code = Reader::from_png(&png_bytes)?;
31//! let payload: &[u8] = code.payload();
32//! # Ok::<(), roka_qr::Error>(())
33//! ```
34//!
35//! # Highlights
36//!
37//! - **Zero external crate dependencies** — `std` only.
38//! - **Encode and decode in one crate** — fills a gap on crates.io.
39//! - **Self-contained image I/O** — PNG (encode + decode via built-in DEFLATE
40//!   inflate) and PBM P1/P4.
41//! - **Round-trip tested** against `qrencode` and `zbarimg`.
42//! - **No `unsafe`**.
43//!
44//! [ISO/IEC 18004]: https://www.iso.org/standard/62021.html
45
46// ───── internal modules (codec building blocks) ─────
47mod bch;
48mod decode;
49mod deflate;
50mod deflate_encode;
51mod encode;
52mod galois;
53mod mask;
54mod matrix;
55mod pbm;
56mod png;
57mod reed_solomon;
58mod render;
59mod sampler;
60mod tables;
61
62// ───── public API ─────
63
64pub use bch::EcLevel;
65pub use tables::Version;
66
67/// Errors produced by `roka-qr`.
68///
69/// All fallible operations in this crate return [`Result<T, Error>`]. Variants
70/// are intentionally coarse — most callers only need to distinguish "the input
71/// was malformed" from "the QR was readable but couldn't be recovered".
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum Error {
74    /// Payload is larger than what fits in QR version 40 at the chosen EC level.
75    DataTooLarge,
76    /// Image bytes are not a valid PNG / PBM / supported format.
77    InvalidImage(&'static str),
78    /// The QR code was found in the image, but its data could not be recovered
79    /// (too many errors, or unsupported encoding mode).
80    Corrupted(&'static str),
81    /// The QR uses a feature this crate does not implement (e.g. kanji mode).
82    Unsupported(&'static str),
83}
84
85impl core::fmt::Display for Error {
86    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87        match self {
88            Error::DataTooLarge => f.write_str("data too large for QR (max version 40)"),
89            Error::InvalidImage(msg) => write!(f, "invalid image: {msg}"),
90            Error::Corrupted(msg) => write!(f, "corrupted QR code: {msg}"),
91            Error::Unsupported(msg) => write!(f, "unsupported feature: {msg}"),
92        }
93    }
94}
95
96impl std::error::Error for Error {}
97
98/// Builder for QR code generation.
99///
100/// # Example
101///
102/// ```
103/// use roka_qr::{Encoder, EcLevel};
104///
105/// let code = Encoder::new(b"hello")
106///     .ec_level(EcLevel::M)
107///     .build()?;
108/// assert_eq!(code.version().0, 1);
109/// # Ok::<(), roka_qr::Error>(())
110/// ```
111pub struct Encoder<'a> {
112    data: &'a [u8],
113    ec_level: EcLevel,
114}
115
116impl<'a> Encoder<'a> {
117    /// Start building a QR code for the given payload bytes.
118    ///
119    /// Byte mode is used; any 8-bit data is accepted (UTF-8 strings, otpauth
120    /// URIs, arbitrary binary, etc.).
121    pub fn new(data: &'a [u8]) -> Self {
122        Self {
123            data,
124            ec_level: EcLevel::M,
125        }
126    }
127
128    /// Set the error-correction level (default `EcLevel::M`).
129    pub fn ec_level(mut self, level: EcLevel) -> Self {
130        self.ec_level = level;
131        self
132    }
133
134    /// Encode and return a [`Code`].
135    ///
136    /// The smallest version that fits the data at the chosen EC level is
137    /// selected automatically.
138    pub fn build(self) -> Result<Code, Error> {
139        let (matrix, version, mask) =
140            encode::encode(self.data, self.ec_level).map_err(|_| Error::DataTooLarge)?;
141        Ok(Code {
142            matrix,
143            version,
144            ec_level: self.ec_level,
145            mask,
146            payload: None,
147        })
148    }
149}
150
151/// An encoded or decoded QR code.
152///
153/// Holds the full module matrix plus metadata (version, EC level, mask number).
154/// Convert to a renderable image with [`Code::render`].
155///
156/// `payload` is populated by [`Reader`]; for codes returned from [`Encoder`]
157/// the input bytes are not retained — refer to the original value instead.
158#[derive(Clone)]
159pub struct Code {
160    matrix: matrix::Matrix,
161    version: Version,
162    ec_level: EcLevel,
163    mask: u8,
164    payload: Option<Vec<u8>>,
165}
166
167impl Code {
168    /// QR version (1–40).
169    pub fn version(&self) -> Version {
170        self.version
171    }
172
173    /// Error-correction level used.
174    pub fn ec_level(&self) -> EcLevel {
175        self.ec_level
176    }
177
178    /// Mask pattern (0–7) selected by the encoder or recovered from format info.
179    pub fn mask(&self) -> u8 {
180        self.mask
181    }
182
183    /// Side length of the module matrix in modules. Equals `17 + 4 * version.0`.
184    pub fn size(&self) -> usize {
185        self.matrix.size
186    }
187
188    /// Read a single module. `true` = dark.
189    pub fn module(&self, row: usize, col: usize) -> bool {
190        self.matrix.get(row, col)
191    }
192
193    /// Recovered payload bytes (only meaningful for decoded codes).
194    ///
195    /// For codes returned by [`Encoder::build`] this is empty — the encoder does
196    /// not retain the input.
197    pub fn payload(&self) -> &[u8] {
198        self.payload.as_deref().unwrap_or(&[])
199    }
200
201    /// Start building a [`Bitmap`] from this code.
202    pub fn render(&self) -> RenderBuilder<'_> {
203        RenderBuilder {
204            code: self,
205            scale: 4,
206            quiet_zone: 4,
207        }
208    }
209}
210
211/// Builder returned by [`Code::render`].
212pub struct RenderBuilder<'a> {
213    code: &'a Code,
214    scale: usize,
215    quiet_zone: usize,
216}
217
218impl<'a> RenderBuilder<'a> {
219    /// Scale factor in pixels per module (default 4).
220    pub fn scale(mut self, scale: usize) -> Self {
221        self.scale = scale.max(1);
222        self
223    }
224
225    /// Quiet-zone width in modules (default 4, the QR standard recommendation).
226    pub fn quiet_zone(mut self, quiet: usize) -> Self {
227        self.quiet_zone = quiet;
228        self
229    }
230
231    /// Render the bitmap.
232    pub fn build(self) -> Bitmap {
233        let bm = render::render_to_bitmap(&self.code.matrix, self.scale, self.quiet_zone);
234        Bitmap { inner: bm }
235    }
236}
237
238/// A binary bitmap — `true` = dark pixel.
239#[derive(Clone, Debug, PartialEq, Eq)]
240pub struct Bitmap {
241    inner: pbm::Bitmap,
242}
243
244impl Bitmap {
245    /// Image width in pixels.
246    pub fn width(&self) -> usize {
247        self.inner.width
248    }
249
250    /// Image height in pixels.
251    pub fn height(&self) -> usize {
252        self.inner.height
253    }
254
255    /// Pixel at (x, y); `true` = dark.
256    pub fn pixel(&self, x: usize, y: usize) -> bool {
257        self.inner.get(x, y)
258    }
259
260    /// Encode as PNG (8-bit grayscale).
261    pub fn to_png(&self) -> Vec<u8> {
262        png::encode_grayscale(&self.inner)
263    }
264
265    /// Encode as PBM P1 ASCII text.
266    pub fn to_pbm(&self) -> String {
267        pbm::write_p1(&self.inner)
268    }
269}
270
271/// Decode a QR code from various input formats.
272pub struct Reader;
273
274impl Reader {
275    /// Decode from PNG bytes.
276    pub fn from_png(data: &[u8]) -> Result<Code, Error> {
277        let bm = png::decode(data).map_err(Error::InvalidImage)?;
278        Self::from_bitmap_internal(bm)
279    }
280
281    /// Decode from PBM bytes (P1 ASCII or P4 binary, auto-detected).
282    pub fn from_pbm(data: &[u8]) -> Result<Code, Error> {
283        let bm = pbm::read(data).map_err(Error::InvalidImage)?;
284        Self::from_bitmap_internal(bm)
285    }
286
287    /// Decode from image bytes, auto-detecting PNG vs PBM via magic bytes.
288    pub fn from_image_bytes(data: &[u8]) -> Result<Code, Error> {
289        if data.len() >= 8 && &data[..8] == b"\x89PNG\r\n\x1A\n" {
290            Self::from_png(data)
291        } else {
292            Self::from_pbm(data)
293        }
294    }
295
296    fn from_bitmap_internal(bm: pbm::Bitmap) -> Result<Code, Error> {
297        let matrix = sampler::matrix_from_bitmap(&bm).map_err(Error::Corrupted)?;
298        let version = decode::version_from_size(matrix.size).map_err(Error::Corrupted)?;
299        let (ec_level, mask) = decode::read_format_info(&matrix).map_err(Error::Corrupted)?;
300        let payload = decode::decode(&matrix).map_err(Error::Corrupted)?;
301        Ok(Code {
302            matrix,
303            version,
304            ec_level,
305            mask,
306            payload: Some(payload),
307        })
308    }
309}