Skip to main content

qrcode_generator/
render.rs

1use alloc::{string::String, vec, vec::Vec};
2use core::fmt;
3#[cfg(feature = "std")]
4use std::{
5    io::{self, Write as IoWrite},
6    path::Path,
7};
8
9#[cfg(feature = "std")]
10use atomic_write_file::AtomicWriteFile;
11#[cfg(feature = "image")]
12use image::{
13    ColorType, ImageBuffer, ImageEncoder, Luma,
14    codecs::png::{CompressionType, FilterType, PngEncoder},
15};
16#[cfg(feature = "tokio")]
17use tokio::io::{AsyncWrite as TokioAsyncWrite, AsyncWriteExt};
18
19use crate::{RenderError, Symbol, SymbolVersion};
20
21/// Renders an encoded symbol at exact output dimensions.
22#[derive(Clone, Copy, Debug)]
23pub struct Renderer<'a> {
24    symbol:              &'a Symbol,
25    width:               usize,
26    height:              usize,
27    quiet_zone:          usize,
28    svg_xml_declaration: bool,
29}
30
31impl<'a> Renderer<'a> {
32    /// Creates a renderer with an exact square output size and the standard quiet zone for the symbol family.
33    #[inline]
34    pub const fn new(symbol: &'a Symbol, size: usize) -> Self {
35        Self::new_with_dimensions(symbol, size, size)
36    }
37
38    /// Creates a renderer with exact output dimensions and the standard quiet zone for the symbol family.
39    #[inline]
40    pub const fn new_with_dimensions(symbol: &'a Symbol, width: usize, height: usize) -> Self {
41        let quiet_zone = match symbol.version() {
42            #[cfg(feature = "qr")]
43            SymbolVersion::Qr(_) => 4,
44            #[cfg(feature = "micro-qr")]
45            SymbolVersion::Micro(_) => 2,
46            #[cfg(feature = "rmqr")]
47            SymbolVersion::Rmqr(_) => 2,
48        };
49
50        Self {
51            symbol,
52            width,
53            height,
54            quiet_zone,
55            svg_xml_declaration: true,
56        }
57    }
58
59    /// Sets the minimum quiet zone in modules before any extra centering pixels.
60    #[must_use]
61    #[inline]
62    pub const fn quiet_zone(mut self, modules: usize) -> Self {
63        self.quiet_zone = modules;
64
65        self
66    }
67
68    /// Sets whether SVG output starts with an XML declaration; enabled by default.
69    /// This setting applies to all SVG string, writer and file output methods.
70    #[must_use]
71    #[inline]
72    pub const fn svg_xml_declaration(mut self, enabled: bool) -> Self {
73        self.svg_xml_declaration = enabled;
74
75        self
76    }
77
78    /// Renders an 8-bit grayscale image in row-major order.
79    pub fn to_luma8(self) -> Result<Vec<u8>, RenderError> {
80        let layout = self.layout()?;
81        let length = self.width.checked_mul(self.height).ok_or(RenderError::ImageSizeTooLarge)?;
82        let mut image = vec![255; length];
83        let symbol_width = self.symbol.width();
84        let modules = self.symbol.modules();
85
86        for y in 0..self.symbol.height() {
87            let first_row = layout.margin_y + y * layout.scale;
88            let row_start = first_row * self.width;
89
90            // The row is read directly from the module slice because x and y are always in range here.
91            let module_row = &modules[y * symbol_width..][..symbol_width];
92
93            // Each horizontal run of dark modules is drawn once into the first pixel row.
94            let mut x = 0;
95
96            while x < symbol_width {
97                if !module_row[x] {
98                    x += 1;
99                    continue;
100                }
101
102                let start = x;
103
104                while x < symbol_width && module_row[x] {
105                    x += 1;
106                }
107
108                let output_x = layout.margin_x + start * layout.scale;
109
110                image[row_start + output_x..row_start + output_x + (x - start) * layout.scale]
111                    .fill(0);
112            }
113
114            // The finished pixel row is copied to the remaining rows of this module row.
115            for row in 1..layout.scale {
116                let destination = (first_row + row) * self.width;
117
118                image.copy_within(row_start..row_start + self.width, destination);
119            }
120        }
121
122        Ok(image)
123    }
124
125    /// Writes an SVG document to a writer.
126    ///
127    /// The description is written as XML text; markup characters are escaped and characters that XML 1.0 does not allow are replaced with U+FFFD.
128    /// Pass `None::<&str>` when no description is needed.
129    #[cfg(feature = "std")]
130    pub fn write_svg<W: IoWrite>(
131        self,
132        writer: W,
133        description: Option<impl AsRef<str>>,
134    ) -> Result<(), RenderError> {
135        let description = description.as_ref().map(AsRef::as_ref);
136        let layout = self.layout()?;
137        let mut writer = IoFmtWriter {
138            inner: writer, error: None
139        };
140
141        if self.write_svg_content(&mut writer, description, layout).is_err() {
142            return Err(writer.error.expect("the I/O adapter stores formatting errors").into());
143        }
144
145        writer.inner.flush()?;
146        Ok(())
147    }
148
149    /// Renders an SVG document as a UTF-8 string.
150    ///
151    /// The description is written as XML text; markup characters are escaped and characters that XML 1.0 does not allow are replaced with U+FFFD.
152    /// Pass `None::<&str>` when no description is needed.
153    pub fn to_svg_string(
154        self,
155        description: Option<impl AsRef<str>>,
156    ) -> Result<String, RenderError> {
157        let description = description.as_ref().map(AsRef::as_ref);
158        let layout = self.layout()?;
159        let mut svg = String::with_capacity(8192);
160
161        self.write_svg_content(&mut svg, description, layout)
162            .expect("writing an SVG to a String cannot fail");
163
164        Ok(svg)
165    }
166
167    fn write_svg_content<W: fmt::Write>(
168        self,
169        writer: &mut W,
170        description: Option<&str>,
171        layout: Layout,
172    ) -> fmt::Result {
173        if self.svg_xml_declaration {
174            writer.write_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")?;
175        }
176
177        writeln!(
178            writer,
179            "<svg width=\"{}\" height=\"{}\" shape-rendering=\"crispEdges\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">",
180            self.width, self.height
181        )?;
182
183        if let Some(description) = description {
184            if !description.is_empty() {
185                writer.write_str("\t<desc>")?;
186                write_xml_text(writer, description)?;
187                writer.write_str("</desc>\n")?;
188            }
189        } else {
190            writeln!(
191                writer,
192                "\t<desc>{} {} by magiclen.org</desc>",
193                env!("CARGO_PKG_NAME"),
194                env!("CARGO_PKG_VERSION")
195            )?;
196        }
197
198        write!(
199            writer,
200            "\t<rect width=\"{}\" height=\"{}\" fill=\"#FFF\"/>\n\t<path d=\"",
201            self.width, self.height
202        )?;
203
204        let symbol_width = self.symbol.width();
205        let modules = self.symbol.modules();
206
207        for y in 0..self.symbol.height() {
208            // The row is read directly from the module slice because x and y are always in range here.
209            let module_row = &modules[y * symbol_width..][..symbol_width];
210
211            // One SVG rectangle command represents each horizontal run of dark modules.
212            let mut x = 0;
213
214            while x < symbol_width {
215                if !module_row[x] {
216                    x += 1;
217                    continue;
218                }
219
220                let start = x;
221
222                while x < symbol_width && module_row[x] {
223                    x += 1;
224                }
225
226                let output_x = layout.margin_x + start * layout.scale;
227                let output_y = layout.margin_y + y * layout.scale;
228                let width = (x - start) * layout.scale;
229
230                write!(
231                    writer,
232                    "M{output_x} {output_y}h{width}v{}H{output_x}V{output_y}",
233                    layout.scale
234                )?;
235            }
236        }
237        writer.write_str("\"/>\n</svg>")
238    }
239
240    #[cfg(feature = "tokio")]
241    /// Renders an SVG document in memory, then writes and flushes it to a tokio asynchronous writer.
242    ///
243    /// The description is written as XML text; markup characters are escaped and characters that XML 1.0 does not allow are replaced with U+FFFD.
244    /// Pass `None::<&str>` when no description is needed.
245    pub async fn write_svg_async<W: TokioAsyncWrite + Unpin>(
246        self,
247        mut writer: W,
248        description: Option<impl AsRef<str>>,
249    ) -> Result<(), RenderError> {
250        let svg = self.to_svg_string(description)?;
251
252        writer.write_all(svg.as_bytes()).await?;
253        writer.flush().await?;
254
255        Ok(())
256    }
257
258    /// Atomically saves an SVG document after rendering succeeds.
259    ///
260    /// The description is written as XML text; markup characters are escaped and characters that XML 1.0 does not allow are replaced with U+FFFD.
261    /// Pass `None::<&str>` when no description is needed.
262    #[cfg(feature = "std")]
263    pub fn save_svg(
264        self,
265        path: impl AsRef<Path>,
266        description: Option<impl AsRef<str>>,
267    ) -> Result<(), RenderError> {
268        let mut file = AtomicWriteFile::open(path)?;
269
270        // Buffering coalesces the many small SVG path writes into a few large writes to the file.
271        self.write_svg(io::BufWriter::new(&mut file), description)?;
272
273        file.commit()?;
274
275        Ok(())
276    }
277
278    #[cfg(feature = "tokio")]
279    /// Atomically saves an SVG document after rendering succeeds, offloading the write to tokio's blocking pool.
280    ///
281    /// Like [`save_svg`](Self::save_svg), the write goes through a temporary file, so an existing file is left untouched if it fails.
282    /// The description is written as XML text; markup characters are escaped and characters that XML 1.0 does not allow are replaced with U+FFFD.
283    /// Pass `None::<&str>` when no description is needed.
284    pub async fn save_svg_async(
285        self,
286        path: impl AsRef<Path>,
287        description: Option<impl AsRef<str>>,
288    ) -> Result<(), RenderError> {
289        let svg = self.to_svg_string(description)?;
290
291        save_atomic_blocking(path.as_ref().to_path_buf(), svg.into_bytes()).await
292    }
293
294    #[cfg(feature = "image")]
295    /// Writes a grayscale PNG image to a writer, then flushes it.
296    pub fn write_png<W: IoWrite>(self, writer: W) -> Result<(), RenderError> {
297        let width = u32::try_from(self.width).map_err(|_| RenderError::ImageSizeTooLarge)?;
298        let height = u32::try_from(self.height).map_err(|_| RenderError::ImageSizeTooLarge)?;
299        let image = self.to_luma8()?;
300        let mut writer = ErrorKeepingWriter {
301            inner: writer, error: None
302        };
303        let result =
304            PngEncoder::new_with_quality(&mut writer, CompressionType::Best, FilterType::NoFilter)
305                .write_image(&image, width, height, ColorType::L8.into());
306
307        // The PNG encoder ignores the write error of its final chunk, so the kept error is checked before its result.
308        if let Some(error) = writer.error.take() {
309            return Err(error.into());
310        }
311
312        result?;
313        writer.inner.flush()?;
314        Ok(())
315    }
316
317    #[cfg(feature = "image")]
318    /// Renders a grayscale PNG image into a byte vector.
319    pub fn to_png_vec(self) -> Result<Vec<u8>, RenderError> {
320        let mut bytes = Vec::with_capacity(4096);
321
322        self.write_png(&mut bytes)?;
323
324        Ok(bytes)
325    }
326
327    #[cfg(all(feature = "tokio", feature = "image"))]
328    /// Renders a PNG image in memory, then writes and flushes it to a tokio asynchronous writer.
329    pub async fn write_png_async<W: TokioAsyncWrite + Unpin>(
330        self,
331        mut writer: W,
332    ) -> Result<(), RenderError> {
333        let png = self.to_png_vec()?;
334
335        writer.write_all(&png).await?;
336        writer.flush().await?;
337
338        Ok(())
339    }
340
341    #[cfg(feature = "image")]
342    /// Atomically saves a PNG image after rendering succeeds.
343    pub fn save_png(self, path: impl AsRef<Path>) -> Result<(), RenderError> {
344        let mut file = AtomicWriteFile::open(path)?;
345
346        self.write_png(&mut file)?;
347
348        file.commit()?;
349
350        Ok(())
351    }
352
353    #[cfg(all(feature = "tokio", feature = "image"))]
354    /// Atomically saves a PNG image after rendering succeeds, offloading the write to tokio's blocking pool.
355    ///
356    /// Like [`save_png`](Self::save_png), the write goes through a temporary file, so an existing file is left untouched if it fails.
357    pub async fn save_png_async(self, path: impl AsRef<Path>) -> Result<(), RenderError> {
358        let png = self.to_png_vec()?;
359
360        save_atomic_blocking(path.as_ref().to_path_buf(), png).await
361    }
362
363    #[cfg(feature = "image")]
364    /// Renders a grayscale image buffer.
365    pub fn to_image_buffer(self) -> Result<ImageBuffer<Luma<u8>, Vec<u8>>, RenderError> {
366        let width = u32::try_from(self.width).map_err(|_| RenderError::ImageSizeTooLarge)?;
367        let height = u32::try_from(self.height).map_err(|_| RenderError::ImageSizeTooLarge)?;
368        let image = self.to_luma8()?;
369
370        ImageBuffer::from_vec(width, height, image).ok_or(RenderError::ImageSizeTooLarge)
371    }
372
373    fn layout(self) -> Result<Layout, RenderError> {
374        let quiet_zone_modules =
375            self.quiet_zone.checked_mul(2).ok_or(RenderError::ImageSizeTooLarge)?;
376        let modules_width = self
377            .symbol
378            .width()
379            .checked_add(quiet_zone_modules)
380            .ok_or(RenderError::ImageSizeTooLarge)?;
381        let modules_height = self
382            .symbol
383            .height()
384            .checked_add(quiet_zone_modules)
385            .ok_or(RenderError::ImageSizeTooLarge)?;
386
387        // Integer scaling keeps every module edge aligned to an output pixel boundary.
388        let scale = (self.width / modules_width).min(self.height / modules_height);
389
390        if scale == 0 {
391            return Err(RenderError::ImageSizeTooSmall);
392        }
393
394        let symbol_width =
395            self.symbol.width().checked_mul(scale).ok_or(RenderError::ImageSizeTooLarge)?;
396        let symbol_height =
397            self.symbol.height().checked_mul(scale).ok_or(RenderError::ImageSizeTooLarge)?;
398
399        // Centering splits pixels left over after fitting the requested quiet zone and integer scale.
400        Ok(Layout {
401            scale,
402            margin_x: (self.width - symbol_width) / 2,
403            margin_y: (self.height - symbol_height) / 2,
404        })
405    }
406}
407
408/// Draws the symbol as compact Unicode text using half-block characters, with one module per column and two module rows per line.
409///
410/// The alternate form (`{:#}`) inverts dark and light modules for dark terminal backgrounds.
411/// Pixel dimensions are ignored; only the quiet zone setting applies.
412/// Each line ends with a newline.
413/// Panics if the quiet zone makes the drawing wider or taller than `usize::MAX` modules.
414///
415/// Note: half-block characters have ambiguous East Asian width and may render as two columns in some CJK terminals, causing misalignment.
416impl fmt::Display for Renderer<'_> {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        let visible_dark = !f.alternate();
419        let quiet_zone = self.quiet_zone;
420        let quiet_zone_modules = quiet_zone.checked_mul(2);
421        let with_quiet_zone = |modules: usize| {
422            quiet_zone_modules
423                .and_then(|margin| modules.checked_add(margin))
424                .expect("the quiet zone is too large")
425        };
426        let columns = with_quiet_zone(self.symbol.width());
427        let rows = with_quiet_zone(self.symbol.height());
428
429        for upper in (0..rows).step_by(2) {
430            for x in 0..columns {
431                let visible = |y: usize| {
432                    // Out-of-range lookups return `None`, so the quiet zone reads as light without a boundary check.
433                    let dark = x
434                        .checked_sub(quiet_zone)
435                        .zip(y.checked_sub(quiet_zone))
436                        .and_then(|(x, y)| self.symbol.module(x, y))
437                        == Some(true);
438
439                    dark == visible_dark
440                };
441
442                // The leftover half of an odd final line stays blank in both forms.
443                let lower = upper + 1 < rows && visible(upper + 1);
444
445                f.write_str(match (visible(upper), lower) {
446                    (true, true) => "█",
447                    (true, false) => "▀",
448                    (false, true) => "▄",
449                    (false, false) => " ",
450                })?;
451            }
452
453            f.write_str("\n")?;
454        }
455
456        Ok(())
457    }
458}
459
460// Writes text as XML 1.0 character data.
461// Characters that XML 1.0 does not allow, even as character references, are replaced with U+FFFD.
462fn write_xml_text<W: fmt::Write>(writer: &mut W, text: &str) -> fmt::Result {
463    let mut start = 0;
464
465    for (index, c) in text.char_indices() {
466        let replacement = match c {
467            '&' => "&amp;",
468            '<' => "&lt;",
469            // Escaping `>` keeps `]]>` out of the character data.
470            '>' => "&gt;",
471            // A literal CR would be normalized to LF by XML parsers.
472            '\r' => "&#xD;",
473            '\t' | '\n' => continue,
474            '\u{0}'..='\u{1F}' | '\u{FFFE}' | '\u{FFFF}' => "\u{FFFD}",
475            _ => continue,
476        };
477
478        writer.write_str(&text[start..index])?;
479        writer.write_str(replacement)?;
480        start = index + c.len_utf8();
481    }
482
483    writer.write_str(&text[start..])
484}
485
486#[cfg(feature = "std")]
487struct IoFmtWriter<W> {
488    inner: W,
489    error: Option<io::Error>,
490}
491
492#[cfg(feature = "std")]
493impl<W: IoWrite> fmt::Write for IoFmtWriter<W> {
494    fn write_str(&mut self, text: &str) -> fmt::Result {
495        self.inner.write_all(text.as_bytes()).map_err(|error| {
496            self.error = Some(error);
497            fmt::Error
498        })
499    }
500}
501
502// Keeps the first write error, because an encoder may ignore errors that it cannot return.
503#[cfg(feature = "image")]
504struct ErrorKeepingWriter<W> {
505    inner: W,
506    error: Option<io::Error>,
507}
508
509#[cfg(feature = "image")]
510impl<W: IoWrite> ErrorKeepingWriter<W> {
511    fn keep<T>(&mut self, result: io::Result<T>) -> io::Result<T> {
512        result.map_err(|error| {
513            let kind = error.kind();
514
515            self.error.get_or_insert(error);
516            kind.into()
517        })
518    }
519}
520
521#[cfg(feature = "image")]
522impl<W: IoWrite> IoWrite for ErrorKeepingWriter<W> {
523    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
524        match self.inner.write(bytes) {
525            // An interrupted write is retried by `write_all`, so it is not a failure yet.
526            Err(error) if error.kind() == io::ErrorKind::Interrupted => Err(error),
527            result => self.keep(result),
528        }
529    }
530
531    fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {
532        let result = self.inner.write_all(bytes);
533
534        self.keep(result)
535    }
536
537    fn flush(&mut self) -> io::Result<()> {
538        let result = self.inner.flush();
539
540        self.keep(result)
541    }
542}
543
544// atomic-write-file has no async backend, so the atomic save runs on tokio's blocking pool.
545#[cfg(feature = "tokio")]
546async fn save_atomic_blocking(path: std::path::PathBuf, bytes: Vec<u8>) -> Result<(), RenderError> {
547    let write = tokio::task::spawn_blocking(move || {
548        let mut file = AtomicWriteFile::open(path)?;
549
550        file.write_all(&bytes)?;
551
552        file.commit()
553    })
554    .await;
555
556    match write {
557        Ok(result) => result.map_err(RenderError::from),
558        Err(join_error) => Err(RenderError::Io(io::Error::other(join_error))),
559    }
560}
561
562struct Layout {
563    scale:    usize,
564    margin_x: usize,
565    margin_y: usize,
566}