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 image = self.to_luma8()?;
282
283 let width = u32::try_from(self.width).map_err(|_| RenderError::ImageSizeTooLarge)?;
284 let height = u32::try_from(self.height).map_err(|_| RenderError::ImageSizeTooLarge)?;
285
286 PngEncoder::new_with_quality(writer, CompressionType::Best, FilterType::NoFilter)
287 .write_image(&image, width, height, ColorType::L8.into())?;
288
289 Ok(())
290 }
291
292 #[cfg(feature = "image")]
293 pub fn to_png_vec(self) -> Result<Vec<u8>, RenderError> {
295 let mut bytes = Vec::with_capacity(4096);
296
297 self.write_png(&mut bytes)?;
298
299 Ok(bytes)
300 }
301
302 #[cfg(all(feature = "tokio", feature = "image"))]
303 pub async fn write_png_async<W: TokioAsyncWrite + Unpin>(
305 self,
306 mut writer: W,
307 ) -> Result<(), RenderError> {
308 let png = self.to_png_vec()?;
309
310 writer.write_all(&png).await?;
311 writer.flush().await?;
312
313 Ok(())
314 }
315
316 #[cfg(feature = "image")]
317 pub fn save_png(self, path: impl AsRef<Path>) -> Result<(), RenderError> {
319 let mut file = AtomicWriteFile::open(path)?;
320
321 self.write_png(&mut file)?;
322
323 file.commit()?;
324
325 Ok(())
326 }
327
328 #[cfg(all(feature = "tokio", feature = "image"))]
329 pub async fn save_png_async(self, path: impl AsRef<Path>) -> Result<(), RenderError> {
333 let png = self.to_png_vec()?;
334
335 save_atomic_blocking(path.as_ref().to_path_buf(), png).await
336 }
337
338 #[cfg(feature = "image")]
339 pub fn to_image_buffer(self) -> Result<ImageBuffer<Luma<u8>, Vec<u8>>, RenderError> {
341 let image = self.to_luma8()?;
342
343 let width = u32::try_from(self.width).map_err(|_| RenderError::ImageSizeTooLarge)?;
344 let height = u32::try_from(self.height).map_err(|_| RenderError::ImageSizeTooLarge)?;
345
346 ImageBuffer::from_vec(width, height, image).ok_or(RenderError::ImageSizeTooLarge)
347 }
348
349 fn layout(self) -> Result<Layout, RenderError> {
350 let quiet_zone_modules =
351 self.quiet_zone.checked_mul(2).ok_or(RenderError::ImageSizeTooLarge)?;
352 let modules_width = self
353 .symbol
354 .width()
355 .checked_add(quiet_zone_modules)
356 .ok_or(RenderError::ImageSizeTooLarge)?;
357 let modules_height = self
358 .symbol
359 .height()
360 .checked_add(quiet_zone_modules)
361 .ok_or(RenderError::ImageSizeTooLarge)?;
362
363 let scale = (self.width / modules_width).min(self.height / modules_height);
365
366 if scale == 0 {
367 return Err(RenderError::ImageSizeTooSmall);
368 }
369
370 let symbol_width =
371 self.symbol.width().checked_mul(scale).ok_or(RenderError::ImageSizeTooLarge)?;
372 let symbol_height =
373 self.symbol.height().checked_mul(scale).ok_or(RenderError::ImageSizeTooLarge)?;
374
375 Ok(Layout {
377 scale,
378 margin_x: (self.width - symbol_width) / 2,
379 margin_y: (self.height - symbol_height) / 2,
380 })
381 }
382}
383
384impl fmt::Display for Renderer<'_> {
392 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393 let visible_dark = !f.alternate();
394 let quiet_zone = self.quiet_zone;
395 let columns = self.symbol.width() + quiet_zone * 2;
396 let rows = self.symbol.height() + quiet_zone * 2;
397
398 for upper in (0..rows).step_by(2) {
399 for x in 0..columns {
400 let visible = |y: usize| {
401 let dark = x
403 .checked_sub(quiet_zone)
404 .zip(y.checked_sub(quiet_zone))
405 .and_then(|(x, y)| self.symbol.module(x, y))
406 == Some(true);
407
408 dark == visible_dark
409 };
410
411 let lower = upper + 1 < rows && visible(upper + 1);
413
414 f.write_str(match (visible(upper), lower) {
415 (true, true) => "█",
416 (true, false) => "▀",
417 (false, true) => "▄",
418 (false, false) => " ",
419 })?;
420 }
421
422 f.write_str("\n")?;
423 }
424
425 Ok(())
426 }
427}
428
429#[cfg(feature = "std")]
430struct IoFmtWriter<W> {
431 inner: W,
432 error: Option<io::Error>,
433}
434
435#[cfg(feature = "std")]
436impl<W: IoWrite> fmt::Write for IoFmtWriter<W> {
437 fn write_str(&mut self, text: &str) -> fmt::Result {
438 self.inner.write_all(text.as_bytes()).map_err(|error| {
439 self.error = Some(error);
440 fmt::Error
441 })
442 }
443}
444
445#[cfg(feature = "tokio")]
447async fn save_atomic_blocking(path: std::path::PathBuf, bytes: Vec<u8>) -> Result<(), RenderError> {
448 let write = tokio::task::spawn_blocking(move || {
449 let mut file = AtomicWriteFile::open(path)?;
450
451 file.write_all(&bytes)?;
452
453 file.commit()
454 })
455 .await;
456
457 match write {
458 Ok(result) => result.map_err(RenderError::from),
459 Err(join_error) => Err(RenderError::Io(io::Error::other(join_error))),
460 }
461}
462
463struct Layout {
464 scale: usize,
465 margin_x: usize,
466 margin_y: usize,
467}