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}