Skip to main content

qrcode_render/
lib.rs

1//! Rendering pipeline for converting QR codes into visual output.
2//!
3//! This module provides the [`Pixel`] and [`Canvas`] traits that abstract over
4//! different output formats, and the [`Renderer`] builder that drives the
5//! conversion from QR code data to a final image.
6//!
7//! # Supported formats
8//!
9//! | Module  | Feature | Output type |
10//! |---------|---------|-------------|
11//! | `image` | `image` | PNG/JPEG via the `image` crate |
12//! | `svg`   | `svg`   | SVG XML string |
13//! | `eps`   | `eps`   | Encapsulated PostScript |
14//! | `html`  | `html`  | HTML table or CSS Grid |
15//! | `pic`   | `pic`   | PIC (troff) macros |
16//! | `string`| —       | Plain text with custom characters |
17//! | `unicode`| —      | Unicode block-element rendering |
18//!
19//! # Custom rendering
20//!
21//! Implement [`Pixel`] for your own type to render into a custom format.
22//! The [`Pixel`] trait defines how to create dark/light pixels and how to
23//! finalize a canvas into a concrete image.
24
25#![cfg_attr(not(feature = "std"), no_std)]
26
27extern crate alloc;
28
29#[cfg(not(feature = "std"))]
30#[allow(unused_imports)]
31use alloc::{
32    borrow::ToOwned,
33    format,
34    string::{String, ToString},
35    vec,
36    vec::Vec,
37};
38
39use core::cmp::max;
40use core::fmt;
41use qrcode_core::As;
42pub use qrcode_core::Color;
43
44pub mod ansi;
45pub mod colors;
46#[cfg(feature = "image")]
47pub mod image;
48pub mod plugin;
49pub mod string;
50pub mod unicode;
51
52//------------------------------------------------------------------------------
53//{{{ Pixel trait
54
55/// Abstraction of an image pixel.
56pub trait Pixel: Copy + Sized {
57    /// Type of the finalized image.
58    type Image: Sized + 'static;
59
60    /// The type that stores an intermediate buffer before finalizing to a
61    /// concrete image
62    type Canvas: Canvas<Pixel = Self, Image = Self::Image>;
63
64    /// Obtains the default module size. The result must be at least 1×1.
65    fn default_unit_size() -> (u32, u32) {
66        (8, 8)
67    }
68
69    /// Obtains the default pixel color when a module is dark or light.
70    fn default_color(color: Color) -> Self;
71}
72
73/// A [`Pixel`] constructible from a CSS-style hex color string (`"#rrggbb"` or
74/// `"#rgb"`), used by `Renderer::template` to apply a `QrTemplate`.
75///
76/// Implemented for the owned, styled backends (image RGB/RGBA, EPS, PDF, ANSI).
77/// The borrowing backends (`svg::Color`, `html::Color`) are not `StyledPixel`
78/// because their color borrows from the input and can't be stored generically;
79/// apply those colors manually instead.
80pub trait StyledPixel: Pixel {
81    /// Builds a pixel from a hex color string, falling back to black on an
82    /// unparseable value.
83    fn from_hex(hex: &str) -> Self;
84}
85
86/// Styling data that can be applied to a [`Renderer`] with
87/// [`Renderer::template`].
88///
89/// The facade crate implements this for its `QrTemplate`, while downstream
90/// crates can provide their own lightweight template types without depending on
91/// the facade.
92pub trait RenderTemplate {
93    /// Dark module color as a CSS hex string.
94    fn dark_color(&self) -> &str;
95
96    /// Light module color as a CSS hex string.
97    fn light_color(&self) -> &str;
98
99    /// Optional module dimensions `(width, height)`.
100    fn module_size(&self) -> Option<(u32, u32)>;
101
102    /// Whether to include the quiet zone.
103    fn quiet_zone(&self) -> bool;
104}
105
106/// Rendering canvas of a QR code image.
107pub trait Canvas: Sized {
108    /// The pixel type stored in this canvas.
109    type Pixel: Sized;
110    /// The finalized image type produced from this canvas.
111    type Image: Sized;
112
113    /// Constructs a new canvas of the given dimensions.
114    fn new(width: u32, height: u32, dark_pixel: Self::Pixel, light_pixel: Self::Pixel) -> Self;
115
116    /// Draws a single dark pixel at the (x, y) coordinate.
117    fn draw_dark_pixel(&mut self, x: u32, y: u32);
118
119    /// Draws a filled dark rectangle covering the given module range. Default
120    /// implementation fills it pixel by pixel; override for a faster path.
121    fn draw_dark_rect(&mut self, left: u32, top: u32, width: u32, height: u32) {
122        for y in top..(top + height) {
123            for x in left..(left + width) {
124                self.draw_dark_pixel(x, y);
125            }
126        }
127    }
128
129    /// Finalize the canvas to a real image.
130    fn into_image(self) -> Self::Image;
131}
132
133//}}}
134//------------------------------------------------------------------------------
135//{{{ Renderer
136
137/// Errors returned by fallible render construction or rendering.
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum RenderError {
140    /// The module source is not a non-empty square row-major QR module grid.
141    InvalidModuleSource {
142        /// Source width in modules.
143        width: usize,
144        /// Source height in modules.
145        height: usize,
146        /// Number of row-major modules exposed by the source.
147        len: usize,
148    },
149
150    /// The module source is wider than this renderer can represent internally.
151    ModuleSourceTooWide {
152        /// Source width in modules.
153        width: usize,
154    },
155}
156
157impl fmt::Display for RenderError {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            RenderError::InvalidModuleSource { width, height, len } => {
161                write!(f, "invalid module source dimensions: width={width}, height={height}, len={len}")
162            }
163            RenderError::ModuleSourceTooWide { width } => write!(f, "module source width {width} exceeds u32::MAX"),
164        }
165    }
166}
167
168#[cfg(feature = "std")]
169impl std::error::Error for RenderError {}
170
171/// A QR code renderer. This is a builder type which converts a bool-vector into
172/// an image.
173pub struct Renderer<'a, P: Pixel> {
174    content: &'a [Color],
175    modules_count: u32, // <- we call it `modules_count` here to avoid ambiguity of `width`.
176    quiet_zone: u32,
177    module_size: (u32, u32),
178
179    dark_color: P,
180    light_color: P,
181    has_quiet_zone: bool,
182}
183
184impl<'a, P: Pixel> Renderer<'a, P> {
185    /// Creates a new renderer.
186    ///
187    /// # Panics
188    /// panics if content is not `modules_count` squared big
189    pub fn new(content: &'a [Color], modules_count: usize, quiet_zone: u32) -> Renderer<'a, P> {
190        assert_eq!(modules_count * modules_count, content.len());
191        Renderer {
192            content,
193            modules_count: modules_count.as_u32(),
194            quiet_zone,
195            module_size: P::default_unit_size(),
196            dark_color: P::default_color(Color::Dark),
197            light_color: P::default_color(Color::Light),
198            has_quiet_zone: true,
199        }
200    }
201
202    /// Creates a new renderer from a module-grid source.
203    ///
204    /// This is the read-only-source counterpart to [`Renderer::new`]. It is
205    /// useful when rendering a borrowed view that implements
206    /// [`qrcode_core::ModuleSource`] but does not expose facade-specific QR code
207    /// methods.
208    ///
209    /// # Panics
210    ///
211    /// Panics if `source` is not square or if its row-major module slice length
212    /// does not match `width() * height()`.
213    pub fn from_source<C>(source: &'a C, quiet_zone: u32) -> Renderer<'a, P>
214    where
215        C: qrcode_core::ModuleSource + ?Sized,
216    {
217        match Self::try_from_source(source, quiet_zone) {
218            Ok(renderer) => renderer,
219            Err(err) => panic!("{err}"),
220        }
221    }
222
223    /// Creates a new renderer from a QR symbol.
224    ///
225    /// This is the metadata-aware counterpart to [`Renderer::from_source`].
226    /// The quiet zone is inferred from [`qrcode_core::QrSymbol::quiet_zone`].
227    ///
228    /// # Panics
229    ///
230    /// Panics if `symbol` exposes an invalid module grid.
231    pub fn from_symbol<S>(symbol: &'a S) -> Renderer<'a, P>
232    where
233        S: qrcode_core::QrSymbol + ?Sized,
234    {
235        Self::from_source(symbol, symbol.quiet_zone())
236    }
237
238    /// Tries to create a new renderer from a module-grid source.
239    ///
240    /// Unlike [`Renderer::from_source`], this constructor reports malformed
241    /// sources as [`RenderError`] instead of panicking.
242    ///
243    /// # Errors
244    ///
245    /// Returns [`RenderError::InvalidModuleSource`] when `source` is empty,
246    /// non-square, or its row-major module slice length does not match
247    /// `width() * height()`. Returns [`RenderError::ModuleSourceTooWide`] when
248    /// the width cannot be represented by this renderer.
249    pub fn try_from_source<C>(source: &'a C, quiet_zone: u32) -> Result<Renderer<'a, P>, RenderError>
250    where
251        C: qrcode_core::ModuleSource + ?Sized,
252    {
253        let width = source.width();
254        let height = source.height();
255        let len = source.modules().len();
256        let Some(expected_len) = width.checked_mul(height) else {
257            return Err(RenderError::InvalidModuleSource { width, height, len });
258        };
259        if width == 0 || width != height || len != expected_len {
260            return Err(RenderError::InvalidModuleSource { width, height, len });
261        }
262        if width > u32::MAX as usize {
263            return Err(RenderError::ModuleSourceTooWide { width });
264        }
265        Ok(Self::new(source.modules(), width, quiet_zone))
266    }
267
268    /// Tries to create a new renderer from a QR symbol.
269    ///
270    /// This is the fallible, metadata-aware counterpart to
271    /// [`Renderer::try_from_source`]. The quiet zone is inferred from
272    /// [`qrcode_core::QrSymbol::quiet_zone`].
273    ///
274    /// # Errors
275    ///
276    /// Returns the same errors as [`Renderer::try_from_source`] when the symbol
277    /// exposes an invalid module grid.
278    pub fn try_from_symbol<S>(symbol: &'a S) -> Result<Renderer<'a, P>, RenderError>
279    where
280        S: qrcode_core::QrSymbol + ?Sized,
281    {
282        Self::try_from_source(symbol, symbol.quiet_zone())
283    }
284
285    /// Sets color of a dark module. Default is opaque black.
286    pub fn dark_color(&mut self, color: P) -> &mut Self {
287        self.dark_color = color;
288        self
289    }
290
291    /// Sets color of a light module. Default is opaque white.
292    pub fn light_color(&mut self, color: P) -> &mut Self {
293        self.light_color = color;
294        self
295    }
296
297    /// Whether to include the quiet zone in the generated image.
298    pub fn quiet_zone(&mut self, has_quiet_zone: bool) -> &mut Self {
299        self.has_quiet_zone = has_quiet_zone;
300        self
301    }
302
303    /// Sets the size of each module in pixels. Default is 8×8.
304    pub fn module_dimensions(&mut self, width: u32, height: u32) -> &mut Self {
305        self.module_size = (max(width, 1), max(height, 1));
306        self
307    }
308
309    /// Sets the minimum total image size in pixels, including the quiet zone if
310    /// applicable. The renderer will try to find the dimension as small as
311    /// possible, such that each module in the QR code has uniform size (no
312    /// distortion).
313    ///
314    /// For instance, a version 1 QR code has 19 modules across including the
315    /// quiet zone. If we request an image of size ≥200×200, we get that each
316    /// module's size should be 11×11, so the actual image size will be 209×209.
317    pub fn min_dimensions(&mut self, width: u32, height: u32) -> &mut Self {
318        let quiet_zone = if self.has_quiet_zone { 2 } else { 0 } * self.quiet_zone;
319        let width_in_modules = self.modules_count + quiet_zone;
320        let unit_width = width.div_ceil(width_in_modules);
321        let unit_height = height.div_ceil(width_in_modules);
322        self.module_dimensions(unit_width, unit_height)
323    }
324
325    /// Sets the maximum total image size in pixels, including the quiet zone if
326    /// applicable. The renderer will try to find the dimension as large as
327    /// possible, such that each module in the QR code has uniform size (no
328    /// distortion).
329    ///
330    /// For instance, a version 1 QR code has 19 modules across including the
331    /// quiet zone. If we request an image of size ≤200×200, we get that each
332    /// module's size should be 10×10, so the actual image size will be 190×190.
333    ///
334    /// The module size is at least 1×1, so if the restriction is too small, the
335    /// final image *can* be larger than the input.
336    pub fn max_dimensions(&mut self, width: u32, height: u32) -> &mut Self {
337        let quiet_zone = if self.has_quiet_zone { 2 } else { 0 } * self.quiet_zone;
338        let width_in_modules = self.modules_count + quiet_zone;
339        let unit_width = width / width_in_modules;
340        let unit_height = height / width_in_modules;
341        self.module_dimensions(unit_width, unit_height)
342    }
343
344    /// Sets dimensions suitable for web display (200×200 pixels minimum).
345    ///
346    /// This is a convenience preset for embedding QR codes in web pages.
347    /// The actual size may be slightly larger to maintain uniform module sizing.
348    pub fn for_web(&mut self) -> &mut Self {
349        self.min_dimensions(200, 200)
350    }
351
352    /// Sets dimensions suitable for printing at the specified DPI.
353    ///
354    /// Targets a 1-inch × 1-inch physical size. For example, at 300 DPI
355    /// the image will be at least 300×300 pixels.
356    ///
357    /// # Arguments
358    ///
359    /// * `dpi` - Dots per inch (common values: 150 for draft, 300 for standard, 600 for high quality)
360    pub fn for_print(&mut self, dpi: u32) -> &mut Self {
361        self.min_dimensions(dpi.max(72), dpi.max(72))
362    }
363
364    /// Sets dimensions suitable for social media platform sharing.
365    ///
366    /// Targets platform-recommended sizes:
367    ///
368    /// | Platform       | Size (px) |
369    /// |----------------|-----------|
370    /// | `"twitter"`    | 400×400   |
371    /// | `"facebook"`   | 600×600   |
372    /// | `"instagram"`  | 1080×1080 |
373    /// | `"wechat"`     | 600×600   |
374    /// | Any other      | 400×400   |
375    pub fn for_social(&mut self, platform: &str) -> &mut Self {
376        let size = match platform {
377            "twitter" | "x" => 400,
378            "facebook" | "fb" => 600,
379            "instagram" | "ig" => 1080,
380            "wechat" | "weixin" => 600,
381            _ => 400,
382        };
383        self.min_dimensions(size, size)
384    }
385
386    /// Renders the QR code into an image.
387    pub fn build(&self) -> P::Image {
388        let w = self.modules_count;
389        let qz = if self.has_quiet_zone { self.quiet_zone } else { 0 };
390        let width = w + 2 * qz;
391
392        let (mw, mh) = self.module_size;
393        let real_width = width * mw;
394        let real_height = width * mh;
395
396        let mut canvas = P::Canvas::new(real_width, real_height, self.dark_color, self.light_color);
397        let mut i = 0;
398        for y in 0..width {
399            for x in 0..width {
400                if qz <= x && x < w + qz && qz <= y && y < w + qz {
401                    if self.content[i] != Color::Light {
402                        canvas.draw_dark_rect(x * mw, y * mh, mw, mh);
403                    }
404                    i += 1;
405                }
406            }
407        }
408
409        canvas.into_image()
410    }
411}
412
413impl<C, P> qrcode_core::Renderer<C> for Renderer<'_, P>
414where
415    C: qrcode_core::ModuleSource + ?Sized,
416    P: Pixel,
417{
418    type Output = P::Image;
419    type Error = RenderError;
420
421    fn render(&self, code: &C) -> Result<Self::Output, Self::Error> {
422        let mut renderer = Renderer::try_from_source(code, self.quiet_zone)?;
423        renderer.module_size = self.module_size;
424        renderer.dark_color = self.dark_color;
425        renderer.light_color = self.light_color;
426        renderer.has_quiet_zone = self.has_quiet_zone;
427        Ok(renderer.build())
428    }
429}
430
431impl<'a, P: StyledPixel> Renderer<'a, P> {
432    /// Applies a render template: dark/light colors (via
433    /// [`StyledPixel::from_hex`]), optional module size, and the quiet-zone
434    /// setting.
435    pub fn template<T: RenderTemplate>(mut self, tmpl: &T) -> Self {
436        self.dark_color = P::from_hex(tmpl.dark_color());
437        self.light_color = P::from_hex(tmpl.light_color());
438        if let Some((w, h)) = tmpl.module_size() {
439            self.module_size = (w, h);
440        }
441        self.has_quiet_zone = tmpl.quiet_zone();
442        self
443    }
444}
445
446//}}}
447
448#[cfg(test)]
449mod tests {
450    use super::{RenderError, Renderer};
451    use qrcode_core::{Color, EcLevel, ModuleSource, QrSymbol, Renderer as CoreRenderer, Version};
452
453    struct BadSource {
454        modules: [Color; 4],
455    }
456
457    impl ModuleSource for BadSource {
458        fn get(&self, x: usize, y: usize) -> Color {
459            self.modules[y * self.width() + x]
460        }
461
462        fn width(&self) -> usize {
463            3
464        }
465
466        fn height(&self) -> usize {
467            2
468        }
469
470        fn modules(&self) -> &[Color] {
471            &self.modules
472        }
473    }
474
475    struct SymbolSource {
476        version: Version,
477        modules: [Color; 1],
478    }
479
480    impl ModuleSource for SymbolSource {
481        fn get(&self, _x: usize, _y: usize) -> Color {
482            self.modules[0]
483        }
484
485        fn width(&self) -> usize {
486            1
487        }
488
489        fn height(&self) -> usize {
490            1
491        }
492
493        fn modules(&self) -> &[Color] {
494            &self.modules
495        }
496    }
497
498    impl QrSymbol for SymbolSource {
499        fn version(&self) -> Version {
500            self.version
501        }
502
503        fn error_correction_level(&self) -> EcLevel {
504            EcLevel::M
505        }
506    }
507
508    #[test]
509    fn try_from_source_returns_error_for_invalid_dimensions() {
510        let source = BadSource { modules: [Color::Dark; 4] };
511
512        let result = Renderer::<char>::try_from_source(&source, 0);
513        assert!(matches!(result, Err(RenderError::InvalidModuleSource { width: 3, height: 2, len: 4 })));
514    }
515
516    #[test]
517    fn core_renderer_returns_error_for_invalid_source() {
518        let modules = [Color::Dark, Color::Light, Color::Light, Color::Dark];
519        let renderer = Renderer::<char>::new(&modules, 2, 0);
520        let source = BadSource { modules: [Color::Dark; 4] };
521
522        assert_eq!(
523            CoreRenderer::render(&renderer, &source).unwrap_err(),
524            RenderError::InvalidModuleSource { width: 3, height: 2, len: 4 }
525        );
526    }
527
528    #[test]
529    fn core_renderer_matches_direct_builder_output() {
530        let modules = [Color::Dark, Color::Light, Color::Light, Color::Dark];
531        let source = qrcode_core::ModuleView::new(&modules, 2).unwrap();
532        let mut renderer = Renderer::<char>::new(&modules, 2, 1);
533        renderer.dark_color('#').light_color('.');
534
535        assert_eq!(CoreRenderer::render(&renderer, &source).unwrap(), renderer.build());
536    }
537
538    #[test]
539    fn from_symbol_uses_normal_qr_quiet_zone() {
540        let source = SymbolSource { version: Version::Normal(1), modules: [Color::Dark] };
541
542        let output = Renderer::<char>::from_symbol(&source).dark_color('#').light_color('.').build();
543
544        assert_eq!(output.lines().next().map(str::len), Some(9));
545    }
546
547    #[test]
548    fn from_symbol_uses_micro_qr_quiet_zone() {
549        let source = SymbolSource { version: Version::Micro(1), modules: [Color::Dark] };
550
551        let output = Renderer::<char>::from_symbol(&source).dark_color('#').light_color('.').build();
552
553        assert_eq!(output.lines().next().map(str::len), Some(5));
554    }
555}