1use alloc::{boxed::Box, string::String};
4use qrcode_core::{
5 Color, DynRenderer, ModuleSource, ModuleStorage, PluginError, PluginRegistry, PostProcessor, QrPlugin,
6 RenderConfig, RenderOutput, RendererFactory,
7};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub struct PlainTextRendererPlugin;
12
13impl PlainTextRendererPlugin {
14 pub const RENDERER_NAME: &'static str = "plain-text";
16}
17
18impl QrPlugin for PlainTextRendererPlugin {
19 fn name(&self) -> &str {
20 "qrcode-render/plain-text"
21 }
22
23 fn version(&self) -> &str {
24 env!("CARGO_PKG_VERSION")
25 }
26
27 fn register(&self, registry: &mut PluginRegistry) {
28 registry.register_renderer(Self::RENDERER_NAME, Box::new(PlainTextRendererFactory));
29 }
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
34pub struct InvertModulesPlugin;
35
36impl QrPlugin for InvertModulesPlugin {
37 fn name(&self) -> &str {
38 "qrcode-render/invert-modules"
39 }
40
41 fn version(&self) -> &str {
42 env!("CARGO_PKG_VERSION")
43 }
44
45 fn register(&self, registry: &mut PluginRegistry) {
46 registry.register_postprocessor(Box::new(InvertModulesPostProcessor));
47 }
48}
49
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub struct InvertModulesPostProcessor;
53
54impl PostProcessor for InvertModulesPostProcessor {
55 fn process(&self, modules: &mut dyn ModuleStorage) -> Result<(), PluginError> {
56 for y in 0..modules.height() {
57 for x in 0..modules.width() {
58 modules.set(x, y, !modules.get(x, y));
59 }
60 }
61 Ok(())
62 }
63}
64
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
67pub struct PlainTextRendererFactory;
68
69impl RendererFactory for PlainTextRendererFactory {
70 fn build(&self, config: &RenderConfig) -> Box<dyn DynRenderer> {
71 let dark = config_char(config, "dark", '#');
72 let light = config_char(config, "light", ' ');
73 let quiet_zone = config_u32(config, "quiet_zone", 4);
74 Box::new(PlainTextRenderer { dark, light, quiet_zone })
75 }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct PlainTextRenderer {
81 dark: char,
82 light: char,
83 quiet_zone: u32,
84}
85
86impl DynRenderer for PlainTextRenderer {
87 fn render(&self, code: &dyn ModuleSource) -> Result<RenderOutput, PluginError> {
88 qrcode_core::Renderer::render(self, code).map(RenderOutput::Text)
89 }
90}
91
92impl<Code> qrcode_core::Renderer<Code> for PlainTextRenderer
93where
94 Code: ModuleSource + ?Sized,
95{
96 type Output = String;
97 type Error = PluginError;
98
99 fn render(&self, code: &Code) -> Result<Self::Output, Self::Error> {
100 validate_module_source(code)?;
101 Ok(render_plain_text(code, self.dark, self.light, self.quiet_zone))
102 }
103}
104
105fn validate_module_source<Code>(code: &Code) -> Result<(), PluginError>
106where
107 Code: ModuleSource + ?Sized,
108{
109 let width = code.width();
110 let height = code.height();
111 match width.checked_mul(height) {
112 Some(len) if width != 0 && width == height && len == code.modules().len() => Ok(()),
113 _ => Err(PluginError::InvalidModuleGrid),
114 }
115}
116
117fn render_plain_text<Code>(code: &Code, dark: char, light: char, quiet_zone: u32) -> String
118where
119 Code: ModuleSource + ?Sized,
120{
121 let width = code.width();
122 let quiet_zone = quiet_zone as usize;
123 let total_width = width + 2 * quiet_zone;
124 let mut output = String::with_capacity(total_width * total_width + total_width.saturating_sub(1));
125
126 for y in 0..total_width {
127 if y > 0 {
128 output.push('\n');
129 }
130
131 let row = (quiet_zone..quiet_zone + width).contains(&y).then(|| code.row(y - quiet_zone));
132 for x in 0..total_width {
133 let color = row
134 .filter(|_| (quiet_zone..quiet_zone + width).contains(&x))
135 .map_or(Color::Light, |row| row[x - quiet_zone]);
136 output.push(color.select(dark, light));
137 }
138 }
139
140 output
141}
142
143fn config_char(config: &RenderConfig, key: &str, default: char) -> char {
144 config.option(key).and_then(|value| value.chars().next()).unwrap_or(default)
145}
146
147fn config_u32(config: &RenderConfig, key: &str, default: u32) -> u32 {
148 config.option(key).and_then(|value| value.parse().ok()).unwrap_or(default)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::{InvertModulesPlugin, InvertModulesPostProcessor, PlainTextRendererFactory, PlainTextRendererPlugin};
154 use qrcode_core::{
155 Color, ModuleGrid, ModuleSource, PluginError, PluginRegistry, PostProcessor, QrPlugin, RenderConfig,
156 RenderOutput, Renderer as CoreRenderer, RendererFactory,
157 };
158
159 struct BadSource {
160 modules: [Color; 4],
161 }
162
163 impl ModuleSource for BadSource {
164 fn get(&self, x: usize, y: usize) -> Color {
165 self.modules[y * self.width() + x]
166 }
167
168 fn width(&self) -> usize {
169 3
170 }
171
172 fn height(&self) -> usize {
173 2
174 }
175
176 fn modules(&self) -> &[Color] {
177 &self.modules
178 }
179 }
180
181 #[test]
182 fn plugin_registers_plain_text_renderer() {
183 let mut registry = PluginRegistry::new();
184 PlainTextRendererPlugin.register(&mut registry);
185
186 assert!(registry.renderer(PlainTextRendererPlugin::RENDERER_NAME).is_some());
187 }
188
189 #[test]
190 fn invert_modules_plugin_registers_postprocessor() {
191 let mut registry = PluginRegistry::new();
192 InvertModulesPlugin.register(&mut registry);
193
194 assert_eq!(registry.postprocessors().len(), 1);
195 }
196
197 #[test]
198 fn invert_modules_postprocessor_flips_all_modules() {
199 let mut modules =
200 ModuleGrid::new(alloc::vec![Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 2).unwrap();
201
202 InvertModulesPostProcessor.process(&mut modules).unwrap();
203
204 assert_eq!(modules.modules(), [Color::Light, Color::Dark, Color::Dark, Color::Light]);
205 }
206
207 #[test]
208 fn plain_text_renderer_uses_configured_colors_and_quiet_zone() {
209 let modules = ModuleGrid::new(alloc::vec![Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 2).unwrap();
210 let renderer = PlainTextRendererFactory.build(
211 &RenderConfig::new().with_option("dark", "X").with_option("light", ".").with_option("quiet_zone", "0"),
212 );
213
214 assert_eq!(renderer.render(&modules).unwrap(), RenderOutput::Text("X.\n.X".into()));
215 }
216
217 #[test]
218 fn plain_text_core_renderer_matches_dyn_renderer() {
219 let modules = ModuleGrid::new(alloc::vec![Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 2).unwrap();
220 let renderer = super::PlainTextRenderer { dark: 'X', light: '.', quiet_zone: 0 };
221
222 let core_output = CoreRenderer::render(&renderer, &modules).unwrap();
223 let dyn_output = qrcode_core::DynRenderer::render(&renderer, &modules).unwrap();
224
225 assert_eq!(core_output, "X.\n.X");
226 assert_eq!(dyn_output, RenderOutput::Text(core_output));
227 }
228
229 #[test]
230 fn plain_text_renderer_reports_invalid_module_source() {
231 let renderer = PlainTextRendererFactory.build(&RenderConfig::new());
232
233 assert_eq!(renderer.render(&BadSource { modules: [Color::Dark; 4] }), Err(PluginError::InvalidModuleGrid));
234 }
235}