Skip to main content

qrcode_render/
string.rs

1//! String rendering support.
2
3#[cfg(not(feature = "std"))]
4#[allow(unused_imports)]
5use alloc::{
6    borrow::ToOwned,
7    format,
8    string::{String, ToString},
9    vec,
10    vec::Vec,
11};
12
13use crate::{Canvas as RenderCanvas, Pixel, RenderError, check_buffer_size, checked_area};
14use qrcode_core::Color;
15
16/// A renderable character or string fragment used by the plain-text renderer.
17///
18/// Implemented for `char` and `&'static str` so a [`Renderer`](crate::Renderer)
19/// can be driven with either.
20pub trait Element: Copy {
21    /// Returns the default element for a dark or light module.
22    fn default_color(color: Color) -> Self;
23    /// The UTF-8 byte count appended by [`Element::append_to_string`].
24    ///
25    /// Implementations must report this accurately for output-size budgeting.
26    fn strlen(self) -> usize;
27    /// Appends this element to `string`.
28    fn append_to_string(self, string: &mut String);
29}
30
31impl Element for char {
32    fn default_color(color: Color) -> Self {
33        color.select('\u{2588}', ' ')
34    }
35
36    fn strlen(self) -> usize {
37        self.len_utf8()
38    }
39
40    fn append_to_string(self, string: &mut String) {
41        string.push(self);
42    }
43}
44
45impl Element for &str {
46    fn default_color(color: Color) -> Self {
47        color.select("\u{2588}", " ")
48    }
49
50    fn strlen(self) -> usize {
51        self.len()
52    }
53
54    fn append_to_string(self, string: &mut String) {
55        string.push_str(self);
56    }
57}
58
59#[doc(hidden)]
60pub struct Canvas<P: Element> {
61    buffer: Vec<P>,
62    width: usize,
63    dark_pixel: P,
64    dark_byte_len: usize,
65    capacity: usize,
66}
67
68fn layout<P: Element>(width: u32, height: u32, dark: P, light: P) -> Result<(usize, usize), RenderError> {
69    let area = checked_area(width, height)?;
70    let newlines = if area == 0 { 0 } else { height as usize - 1 };
71    let buffer_bytes = area.checked_mul(core::mem::size_of::<P>()).ok_or(RenderError::OutputTooLarge)?;
72    let output_bytes = area
73        .checked_mul(dark.strlen().max(light.strlen()))
74        .and_then(|bytes| bytes.checked_add(newlines))
75        .ok_or(RenderError::OutputTooLarge)?;
76    check_buffer_size(buffer_bytes.checked_add(output_bytes).ok_or(RenderError::OutputTooLarge)?)?;
77    let capacity = area
78        .checked_mul(light.strlen())
79        .and_then(|bytes| bytes.checked_add(newlines))
80        .ok_or(RenderError::OutputTooLarge)?;
81    Ok((area, capacity))
82}
83
84impl<P: Element> Pixel for P {
85    type Image = String;
86    type Canvas = Canvas<Self>;
87
88    fn default_unit_size() -> (u32, u32) {
89        (1, 1)
90    }
91
92    fn default_color(color: Color) -> Self {
93        <Self as Element>::default_color(color)
94    }
95}
96
97impl<P: Element> RenderCanvas for Canvas<P> {
98    type Pixel = P;
99    type Image = String;
100
101    fn new(width: u32, height: u32, dark_pixel: P, light_pixel: P) -> Self {
102        let (area, capacity) = layout(width, height, dark_pixel, light_pixel).unwrap_or_else(|error| panic!("{error}"));
103        Self {
104            buffer: vec![light_pixel; area],
105            width: width as usize,
106            dark_pixel,
107            dark_byte_len: dark_pixel.strlen(),
108            capacity,
109        }
110    }
111
112    fn validate_dimensions(width: u32, height: u32, dark: &P, light: &P) -> Result<(), RenderError> {
113        layout(width, height, *dark, *light).map(|_| ())
114    }
115
116    fn draw_dark_pixel(&mut self, x: u32, y: u32) {
117        let pixel = &mut self.buffer[x as usize + y as usize * self.width];
118        self.capacity = self.capacity - pixel.strlen() + self.dark_byte_len;
119        *pixel = self.dark_pixel;
120    }
121
122    fn into_image(self) -> String {
123        let mut result = String::with_capacity(self.capacity);
124        if self.buffer.is_empty() {
125            return result;
126        }
127        for (row_index, row) in self.buffer.chunks(self.width).enumerate() {
128            if row_index != 0 {
129                result.push('\n');
130            }
131            for &pixel in row {
132                pixel.append_to_string(&mut result);
133            }
134        }
135        result
136    }
137}
138
139#[test]
140fn test_render_to_string() {
141    use crate::Renderer;
142
143    let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark];
144    let image: String = Renderer::<char>::new(colors, 2, 1).build();
145    assert_eq!(&image, "    \n \u{2588}  \n  \u{2588} \n    ");
146
147    let image2 = Renderer::new(colors, 2, 1).light_color("A").dark_color("!B!").module_dimensions(2, 2).build();
148
149    assert_eq!(
150        &image2,
151        "AAAAAAAA\n\
152         AAAAAAAA\n\
153         AA!B!!B!AAAA\n\
154         AA!B!!B!AAAA\n\
155         AAAA!B!!B!AA\n\
156         AAAA!B!!B!AA\n\
157         AAAAAAAA\n\
158         AAAAAAAA"
159    );
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::{MAX_BUFFER_BYTES, Renderer};
166    use core::cell::RefCell;
167
168    #[derive(Clone, Copy)]
169    struct ObservedElement<'a> {
170        value: char,
171        calls: &'a RefCell<Vec<(char, char, usize)>>,
172    }
173
174    impl Element for ObservedElement<'_> {
175        fn default_color(_color: Color) -> Self {
176            panic!("the callback regression uses explicit pixels")
177        }
178
179        fn strlen(self) -> usize {
180            self.calls.borrow_mut().push(('s', self.value, 0));
181            self.value.len_utf8()
182        }
183
184        fn append_to_string(self, output: &mut String) {
185            self.calls.borrow_mut().push(('a', self.value, output.len()));
186            output.push(self.value);
187        }
188    }
189
190    #[test]
191    fn row_serialization_preserves_custom_element_callbacks_and_newline_positions() {
192        let calls = RefCell::new(Vec::new());
193        let dark = ObservedElement { value: '🦀', calls: &calls };
194        let light = ObservedElement { value: 'é', calls: &calls };
195        let mut canvas = Canvas::new(3, 2, dark, light);
196        canvas.draw_dark_pixel(1, 0);
197        canvas.draw_dark_pixel(2, 1);
198        calls.borrow_mut().clear();
199
200        assert_eq!(canvas.into_image(), "é🦀é\néé🦀");
201        assert_eq!(
202            *calls.borrow(),
203            [('a', 'é', 0), ('a', '🦀', 2), ('a', 'é', 6), ('a', 'é', 9), ('a', 'é', 11), ('a', '🦀', 13)]
204        );
205    }
206
207    #[test]
208    fn character_budget_includes_buffer_and_output() {
209        let width = (MAX_BUFFER_BYTES / 5) as u32;
210        assert!(Canvas::<char>::validate_dimensions(width, 1, &'#', &' ').is_ok());
211        assert_eq!(Canvas::<char>::validate_dimensions(width + 1, 1, &'#', &' '), Err(RenderError::OutputTooLarge));
212        assert_eq!(Canvas::<char>::validate_dimensions(65_536, 65_536, &'#', &' '), Err(RenderError::OutputTooLarge));
213    }
214
215    #[test]
216    fn long_utf8_fragments_are_rejected_before_allocating() {
217        let fragment = "💖".repeat(1024);
218        let modules = [Color::Dark];
219        let result = Renderer::new(&modules, 1, 0)
220            .dark_color(fragment.as_str())
221            .light_color("")
222            .module_dimensions(1024, 1024)
223            .try_build();
224        assert_eq!(result, Err(RenderError::OutputTooLarge));
225    }
226
227    #[test]
228    fn repainting_shorter_pixels_keeps_the_actual_output_capacity() {
229        let mut canvas = Canvas::new(2, 2, "", "long");
230        for _ in 0..128 {
231            canvas.draw_dark_pixel(0, 0);
232        }
233        assert_eq!(canvas.capacity, 13);
234        assert_eq!(canvas.into_image(), "long\nlonglong");
235    }
236
237    #[test]
238    fn repainting_multibyte_characters_counts_the_replaced_pixel_once() {
239        let mut canvas = Canvas::new(2, 2, '😀', ' ');
240        for _ in 0..128 {
241            canvas.draw_dark_pixel(1, 1);
242        }
243        assert_eq!(canvas.capacity, 8);
244        assert_eq!(canvas.into_image(), "  \n 😀");
245    }
246
247    #[test]
248    fn empty_canvases_produce_empty_text() {
249        for (width, height) in [(0, 0), (0, u32::MAX), (u32::MAX, 0)] {
250            assert_eq!(Canvas::new(width, height, 'â–ˆ', ' ').into_image(), "");
251        }
252    }
253}