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#[derive(Clone, Copy, Debug)]
23pub struct Renderer<'a> {
24 symbol: &'a Symbol,
25 width: usize,
26 height: usize,
27 quiet_zone: usize,
28}
29
30impl<'a> Renderer<'a> {
31 #[inline]
33 pub const fn new(symbol: &'a Symbol, size: usize) -> Self {
34 Self::new_with_dimensions(symbol, size, size)
35 }
36
37 #[inline]
39 pub const fn new_with_dimensions(symbol: &'a Symbol, width: usize, height: usize) -> Self {
40 let quiet_zone = match symbol.version() {
41 #[cfg(feature = "qr")]
42 SymbolVersion::Qr(_) => 4,
43 #[cfg(feature = "micro-qr")]
44 SymbolVersion::Micro(_) => 2,
45 #[cfg(feature = "rmqr")]
46 SymbolVersion::Rmqr(_) => 2,
47 };
48
49 Self {
50 symbol,
51 width,
52 height,
53 quiet_zone,
54 }
55 }
56
57 #[must_use]
59 #[inline]
60 pub const fn quiet_zone(mut self, modules: usize) -> Self {
61 self.quiet_zone = modules;
62
63 self
64 }
65
66 pub fn to_luma8(self) -> Result<Vec<u8>, RenderError> {
68 let layout = self.layout()?;
69 let length = self.width.checked_mul(self.height).ok_or(RenderError::ImageSizeTooLarge)?;
70 let mut image = vec![255; length];
71 let symbol_width = self.symbol.width();
72 let modules = self.symbol.modules();
73
74 for y in 0..self.symbol.height() {
75 let first_row = layout.margin_y + y * layout.scale;
76 let row_start = first_row * self.width;
77
78 let module_row = &modules[y * symbol_width..][..symbol_width];
80
81 let mut x = 0;
83
84 while x < symbol_width {
85 if !module_row[x] {
86 x += 1;
87 continue;
88 }
89
90 let start = x;
91
92 while x < symbol_width && module_row[x] {
93 x += 1;
94 }
95
96 let output_x = layout.margin_x + start * layout.scale;
97
98 image[row_start + output_x..row_start + output_x + (x - start) * layout.scale]
99 .fill(0);
100 }
101
102 for row in 1..layout.scale {
104 let destination = (first_row + row) * self.width;
105
106 image.copy_within(row_start..row_start + self.width, destination);
107 }
108 }
109
110 Ok(image)
111 }
112
113 #[cfg(feature = "std")]
118 pub fn write_svg<W: IoWrite>(
119 self,
120 writer: W,
121 description: Option<impl AsRef<str>>,
122 ) -> Result<(), RenderError> {
123 let description = description.as_ref().map(AsRef::as_ref);
124 let layout = self.layout()?;
125 let mut writer = IoFmtWriter {
126 inner: writer, error: None
127 };
128
129 if self.write_svg_content(&mut writer, description, layout).is_err() {
130 return Err(writer.error.expect("the I/O adapter stores formatting errors").into());
131 }
132
133 writer.inner.flush()?;
134 Ok(())
135 }
136
137 pub fn to_svg_string(
142 self,
143 description: Option<impl AsRef<str>>,
144 ) -> Result<String, RenderError> {
145 let description = description.as_ref().map(AsRef::as_ref);
146 let layout = self.layout()?;
147 let mut svg = String::with_capacity(8192);
148
149 self.write_svg_content(&mut svg, description, layout)
150 .expect("writing an SVG to a String cannot fail");
151
152 Ok(svg)
153 }
154
155 fn write_svg_content<W: fmt::Write>(
156 self,
157 writer: &mut W,
158 description: Option<&str>,
159 layout: Layout,
160 ) -> fmt::Result {
161 write!(
162 writer,
163 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg width=\"{}\" height=\"{}\" shape-rendering=\"crispEdges\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n",
164 self.width, self.height
165 )?;
166
167 if let Some(description) = description {
168 if !description.is_empty() {
169 writer.write_str("\t<desc>")?;
170 writer.write_str(&html_escape::encode_safe(description))?;
171 writer.write_str("</desc>\n")?;
172 }
173 } else {
174 writeln!(
175 writer,
176 "\t<desc>{} {} by magiclen.org</desc>",
177 env!("CARGO_PKG_NAME"),
178 env!("CARGO_PKG_VERSION")
179 )?;
180 }
181
182 write!(
183 writer,
184 "\t<rect width=\"{}\" height=\"{}\" fill=\"#FFF\"/>\n\t<path d=\"",
185 self.width, self.height
186 )?;
187
188 let symbol_width = self.symbol.width();
189 let modules = self.symbol.modules();
190
191 for y in 0..self.symbol.height() {
192 let module_row = &modules[y * symbol_width..][..symbol_width];
194
195 let mut x = 0;
197
198 while x < symbol_width {
199 if !module_row[x] {
200 x += 1;
201 continue;
202 }
203
204 let start = x;
205
206 while x < symbol_width && module_row[x] {
207 x += 1;
208 }
209
210 let output_x = layout.margin_x + start * layout.scale;
211 let output_y = layout.margin_y + y * layout.scale;
212 let width = (x - start) * layout.scale;
213
214 write!(
215 writer,
216 "M{output_x} {output_y}h{width}v{}H{output_x}V{output_y}",
217 layout.scale
218 )?;
219 }
220 }
221 writer.write_str("\"/>\n</svg>")
222 }
223
224 #[cfg(feature = "tokio")]
225 pub async fn write_svg_async<W: TokioAsyncWrite + Unpin>(
230 self,
231 mut writer: W,
232 description: Option<impl AsRef<str>>,
233 ) -> Result<(), RenderError> {
234 let svg = self.to_svg_string(description)?;
235
236 writer.write_all(svg.as_bytes()).await?;
237 writer.flush().await?;
238
239 Ok(())
240 }
241
242 #[cfg(feature = "std")]
247 pub fn save_svg(
248 self,
249 path: impl AsRef<Path>,
250 description: Option<impl AsRef<str>>,
251 ) -> Result<(), RenderError> {
252 let mut file = AtomicWriteFile::open(path)?;
253
254 self.write_svg(io::BufWriter::new(&mut file), description)?;
256
257 file.commit()?;
258
259 Ok(())
260 }
261
262 #[cfg(feature = "tokio")]
263 pub async fn save_svg_async(
269 self,
270 path: impl AsRef<Path>,
271 description: Option<impl AsRef<str>>,
272 ) -> Result<(), RenderError> {
273 let svg = self.to_svg_string(description)?;
274
275 save_atomic_blocking(path.as_ref().to_path_buf(), svg.into_bytes()).await
276 }
277
278 #[cfg(feature = "image")]
279 pub fn write_png<W: IoWrite>(self, writer: W) -> Result<(), RenderError> {
281 let width = u32::try_from(self.width).map_err(|_| RenderError::ImageSizeTooLarge)?;
282 let height = u32::try_from(self.height).map_err(|_| RenderError::ImageSizeTooLarge)?;
283 let image = self.to_luma8()?;
284
285 PngEncoder::new_with_quality(writer, CompressionType::Best, FilterType::NoFilter)
286 .write_image(&image, width, height, ColorType::L8.into())?;
287
288 Ok(())
289 }
290
291 #[cfg(feature = "image")]
292 pub fn to_png_vec(self) -> Result<Vec<u8>, RenderError> {
294 let mut bytes = Vec::with_capacity(4096);
295
296 self.write_png(&mut bytes)?;
297
298 Ok(bytes)
299 }
300
301 #[cfg(all(feature = "tokio", feature = "image"))]
302 pub async fn write_png_async<W: TokioAsyncWrite + Unpin>(
304 self,
305 mut writer: W,
306 ) -> Result<(), RenderError> {
307 let png = self.to_png_vec()?;
308
309 writer.write_all(&png).await?;
310 writer.flush().await?;
311
312 Ok(())
313 }
314
315 #[cfg(feature = "image")]
316 pub fn save_png(self, path: impl AsRef<Path>) -> Result<(), RenderError> {
318 let mut file = AtomicWriteFile::open(path)?;
319
320 self.write_png(&mut file)?;
321
322 file.commit()?;
323
324 Ok(())
325 }
326
327 #[cfg(all(feature = "tokio", feature = "image"))]
328 pub async fn save_png_async(self, path: impl AsRef<Path>) -> Result<(), RenderError> {
332 let png = self.to_png_vec()?;
333
334 save_atomic_blocking(path.as_ref().to_path_buf(), png).await
335 }
336
337 #[cfg(feature = "image")]
338 pub fn to_image_buffer(self) -> Result<ImageBuffer<Luma<u8>, Vec<u8>>, RenderError> {
340 let width = u32::try_from(self.width).map_err(|_| RenderError::ImageSizeTooLarge)?;
341 let height = u32::try_from(self.height).map_err(|_| RenderError::ImageSizeTooLarge)?;
342 let image = self.to_luma8()?;
343
344 ImageBuffer::from_vec(width, height, image).ok_or(RenderError::ImageSizeTooLarge)
345 }
346
347 fn layout(self) -> Result<Layout, RenderError> {
348 let quiet_zone_modules =
349 self.quiet_zone.checked_mul(2).ok_or(RenderError::ImageSizeTooLarge)?;
350 let modules_width = self
351 .symbol
352 .width()
353 .checked_add(quiet_zone_modules)
354 .ok_or(RenderError::ImageSizeTooLarge)?;
355 let modules_height = self
356 .symbol
357 .height()
358 .checked_add(quiet_zone_modules)
359 .ok_or(RenderError::ImageSizeTooLarge)?;
360
361 let scale = (self.width / modules_width).min(self.height / modules_height);
363
364 if scale == 0 {
365 return Err(RenderError::ImageSizeTooSmall);
366 }
367
368 let symbol_width =
369 self.symbol.width().checked_mul(scale).ok_or(RenderError::ImageSizeTooLarge)?;
370 let symbol_height =
371 self.symbol.height().checked_mul(scale).ok_or(RenderError::ImageSizeTooLarge)?;
372
373 Ok(Layout {
375 scale,
376 margin_x: (self.width - symbol_width) / 2,
377 margin_y: (self.height - symbol_height) / 2,
378 })
379 }
380}
381
382impl fmt::Display for Renderer<'_> {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 let visible_dark = !f.alternate();
392 let quiet_zone = self.quiet_zone;
393 let columns = self.symbol.width() + quiet_zone * 2;
394 let rows = self.symbol.height() + quiet_zone * 2;
395
396 for upper in (0..rows).step_by(2) {
397 for x in 0..columns {
398 let visible = |y: usize| {
399 let dark = x
401 .checked_sub(quiet_zone)
402 .zip(y.checked_sub(quiet_zone))
403 .and_then(|(x, y)| self.symbol.module(x, y))
404 == Some(true);
405
406 dark == visible_dark
407 };
408
409 let lower = upper + 1 < rows && visible(upper + 1);
411
412 f.write_str(match (visible(upper), lower) {
413 (true, true) => "█",
414 (true, false) => "▀",
415 (false, true) => "▄",
416 (false, false) => " ",
417 })?;
418 }
419
420 f.write_str("\n")?;
421 }
422
423 Ok(())
424 }
425}
426
427#[cfg(feature = "std")]
428struct IoFmtWriter<W> {
429 inner: W,
430 error: Option<io::Error>,
431}
432
433#[cfg(feature = "std")]
434impl<W: IoWrite> fmt::Write for IoFmtWriter<W> {
435 fn write_str(&mut self, text: &str) -> fmt::Result {
436 self.inner.write_all(text.as_bytes()).map_err(|error| {
437 self.error = Some(error);
438 fmt::Error
439 })
440 }
441}
442
443#[cfg(feature = "tokio")]
445async fn save_atomic_blocking(path: std::path::PathBuf, bytes: Vec<u8>) -> Result<(), RenderError> {
446 let write = tokio::task::spawn_blocking(move || {
447 let mut file = AtomicWriteFile::open(path)?;
448
449 file.write_all(&bytes)?;
450
451 file.commit()
452 })
453 .await;
454
455 match write {
456 Ok(result) => result.map_err(RenderError::from),
457 Err(join_error) => Err(RenderError::Io(io::Error::other(join_error))),
458 }
459}
460
461struct Layout {
462 scale: usize,
463 margin_x: usize,
464 margin_y: usize,
465}