#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use alloc::{
borrow::ToOwned,
format,
string::{String, ToString},
vec,
vec::Vec,
};
use crate::{Canvas as RenderCanvas, Pixel, RenderError, check_buffer_size, checked_area};
use qrcode_core::Color;
pub trait Element: Copy {
fn default_color(color: Color) -> Self;
fn strlen(self) -> usize;
fn append_to_string(self, string: &mut String);
}
impl Element for char {
fn default_color(color: Color) -> Self {
color.select('\u{2588}', ' ')
}
fn strlen(self) -> usize {
self.len_utf8()
}
fn append_to_string(self, string: &mut String) {
string.push(self);
}
}
impl Element for &str {
fn default_color(color: Color) -> Self {
color.select("\u{2588}", " ")
}
fn strlen(self) -> usize {
self.len()
}
fn append_to_string(self, string: &mut String) {
string.push_str(self);
}
}
#[doc(hidden)]
pub struct Canvas<P: Element> {
buffer: Vec<P>,
width: usize,
dark_pixel: P,
dark_byte_len: usize,
capacity: usize,
}
fn layout<P: Element>(width: u32, height: u32, dark: P, light: P) -> Result<(usize, usize), RenderError> {
let area = checked_area(width, height)?;
let newlines = if area == 0 { 0 } else { height as usize - 1 };
let buffer_bytes = area.checked_mul(core::mem::size_of::<P>()).ok_or(RenderError::OutputTooLarge)?;
let output_bytes = area
.checked_mul(dark.strlen().max(light.strlen()))
.and_then(|bytes| bytes.checked_add(newlines))
.ok_or(RenderError::OutputTooLarge)?;
check_buffer_size(buffer_bytes.checked_add(output_bytes).ok_or(RenderError::OutputTooLarge)?)?;
let capacity = area
.checked_mul(light.strlen())
.and_then(|bytes| bytes.checked_add(newlines))
.ok_or(RenderError::OutputTooLarge)?;
Ok((area, capacity))
}
impl<P: Element> Pixel for P {
type Image = String;
type Canvas = Canvas<Self>;
fn default_unit_size() -> (u32, u32) {
(1, 1)
}
fn default_color(color: Color) -> Self {
<Self as Element>::default_color(color)
}
}
impl<P: Element> RenderCanvas for Canvas<P> {
type Pixel = P;
type Image = String;
fn new(width: u32, height: u32, dark_pixel: P, light_pixel: P) -> Self {
let (area, capacity) = layout(width, height, dark_pixel, light_pixel).unwrap_or_else(|error| panic!("{error}"));
Self {
buffer: vec![light_pixel; area],
width: width as usize,
dark_pixel,
dark_byte_len: dark_pixel.strlen(),
capacity,
}
}
fn validate_dimensions(width: u32, height: u32, dark: &P, light: &P) -> Result<(), RenderError> {
layout(width, height, *dark, *light).map(|_| ())
}
fn draw_dark_pixel(&mut self, x: u32, y: u32) {
let pixel = &mut self.buffer[x as usize + y as usize * self.width];
self.capacity = self.capacity - pixel.strlen() + self.dark_byte_len;
*pixel = self.dark_pixel;
}
fn into_image(self) -> String {
let mut result = String::with_capacity(self.capacity);
if self.buffer.is_empty() {
return result;
}
for (row_index, row) in self.buffer.chunks(self.width).enumerate() {
if row_index != 0 {
result.push('\n');
}
for &pixel in row {
pixel.append_to_string(&mut result);
}
}
result
}
}
#[test]
fn test_render_to_string() {
use crate::Renderer;
let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark];
let image: String = Renderer::<char>::new(colors, 2, 1).build();
assert_eq!(&image, " \n \u{2588} \n \u{2588} \n ");
let image2 = Renderer::new(colors, 2, 1).light_color("A").dark_color("!B!").module_dimensions(2, 2).build();
assert_eq!(
&image2,
"AAAAAAAA\n\
AAAAAAAA\n\
AA!B!!B!AAAA\n\
AA!B!!B!AAAA\n\
AAAA!B!!B!AA\n\
AAAA!B!!B!AA\n\
AAAAAAAA\n\
AAAAAAAA"
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MAX_BUFFER_BYTES, Renderer};
use core::cell::RefCell;
#[derive(Clone, Copy)]
struct ObservedElement<'a> {
value: char,
calls: &'a RefCell<Vec<(char, char, usize)>>,
}
impl Element for ObservedElement<'_> {
fn default_color(_color: Color) -> Self {
panic!("the callback regression uses explicit pixels")
}
fn strlen(self) -> usize {
self.calls.borrow_mut().push(('s', self.value, 0));
self.value.len_utf8()
}
fn append_to_string(self, output: &mut String) {
self.calls.borrow_mut().push(('a', self.value, output.len()));
output.push(self.value);
}
}
#[test]
fn row_serialization_preserves_custom_element_callbacks_and_newline_positions() {
let calls = RefCell::new(Vec::new());
let dark = ObservedElement { value: '🦀', calls: &calls };
let light = ObservedElement { value: 'é', calls: &calls };
let mut canvas = Canvas::new(3, 2, dark, light);
canvas.draw_dark_pixel(1, 0);
canvas.draw_dark_pixel(2, 1);
calls.borrow_mut().clear();
assert_eq!(canvas.into_image(), "é🦀é\néé🦀");
assert_eq!(
*calls.borrow(),
[('a', 'é', 0), ('a', '🦀', 2), ('a', 'é', 6), ('a', 'é', 9), ('a', 'é', 11), ('a', '🦀', 13)]
);
}
#[test]
fn character_budget_includes_buffer_and_output() {
let width = (MAX_BUFFER_BYTES / 5) as u32;
assert!(Canvas::<char>::validate_dimensions(width, 1, &'#', &' ').is_ok());
assert_eq!(Canvas::<char>::validate_dimensions(width + 1, 1, &'#', &' '), Err(RenderError::OutputTooLarge));
assert_eq!(Canvas::<char>::validate_dimensions(65_536, 65_536, &'#', &' '), Err(RenderError::OutputTooLarge));
}
#[test]
fn long_utf8_fragments_are_rejected_before_allocating() {
let fragment = "💖".repeat(1024);
let modules = [Color::Dark];
let result = Renderer::new(&modules, 1, 0)
.dark_color(fragment.as_str())
.light_color("")
.module_dimensions(1024, 1024)
.try_build();
assert_eq!(result, Err(RenderError::OutputTooLarge));
}
#[test]
fn repainting_shorter_pixels_keeps_the_actual_output_capacity() {
let mut canvas = Canvas::new(2, 2, "", "long");
for _ in 0..128 {
canvas.draw_dark_pixel(0, 0);
}
assert_eq!(canvas.capacity, 13);
assert_eq!(canvas.into_image(), "long\nlonglong");
}
#[test]
fn repainting_multibyte_characters_counts_the_replaced_pixel_once() {
let mut canvas = Canvas::new(2, 2, '😀', ' ');
for _ in 0..128 {
canvas.draw_dark_pixel(1, 1);
}
assert_eq!(canvas.capacity, 8);
assert_eq!(canvas.into_image(), " \n 😀");
}
#[test]
fn empty_canvases_produce_empty_text() {
for (width, height) in [(0, 0), (0, u32::MAX), (u32::MAX, 0)] {
assert_eq!(Canvas::new(width, height, 'â–ˆ', ' ').into_image(), "");
}
}
}