1#[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, Color, Pixel, RenderError, check_buffer_size, checked_area};
14
15fn layout(
16 width: u32,
17 height: u32,
18 row_group: usize,
19 col_step: usize,
20 glyph_bytes: usize,
21) -> Result<(usize, usize), RenderError> {
22 let area = checked_area(width, height)?;
23 if area == 0 {
24 return Ok((0, 0));
25 }
26 let rows = (height as usize).div_ceil(row_group);
27 let cols = (width as usize).div_ceil(col_step);
28 let capacity = rows
29 .checked_mul(cols)
30 .and_then(|glyphs| glyphs.checked_mul(glyph_bytes))
31 .and_then(|bytes| bytes.checked_add(rows - 1))
32 .ok_or(RenderError::OutputTooLarge)?;
33 check_buffer_size(area.checked_add(capacity).ok_or(RenderError::OutputTooLarge)?)?;
34 Ok((area, capacity))
35}
36
37macro_rules! impl_bit_canvas {
43 ($canvas:ident, $pixel:ident, $row_group:expr, $col_step:expr, $glyph_bytes:expr, $encode:expr) => {
44 #[doc(hidden)]
45 pub struct $canvas {
46 canvas: Vec<u8>,
47 width: u32,
48 height: u32,
49 dark_pixel: u8,
50 output_capacity: usize,
51 }
52
53 impl RenderCanvas for $canvas {
54 type Pixel = $pixel;
55 type Image = String;
56
57 fn new(width: u32, height: u32, dark_pixel: $pixel, light_pixel: $pixel) -> Self {
58 let (area, output_capacity) = layout(width, height, $row_group, $col_step, $glyph_bytes)
59 .unwrap_or_else(|error| panic!("{error}"));
60 let a = vec![light_pixel.value(); area];
61 $canvas { width, height, canvas: a, dark_pixel: dark_pixel.value(), output_capacity }
62 }
63
64 fn validate_dimensions(
65 width: u32,
66 height: u32,
67 _dark: &$pixel,
68 _light: &$pixel,
69 ) -> Result<(), RenderError> {
70 layout(width, height, $row_group, $col_step, $glyph_bytes).map(|_| ())
71 }
72
73 fn draw_dark_pixel(&mut self, x: u32, y: u32) {
74 self.canvas[x as usize + y as usize * self.width as usize] = self.dark_pixel;
75 }
76
77 fn draw_dark_rect(&mut self, left: u32, top: u32, width: u32, height: u32) {
78 if width == 0 || height == 0 {
79 return;
80 }
81 if width == 1 && height == 1 {
82 self.draw_dark_pixel(left, top);
83 return;
84 }
85 assert!(
86 left < self.width && top < self.height && width <= self.width - left && height <= self.height - top,
87 "rectangle exceeds canvas dimensions"
88 );
89 let stride = self.width as usize;
90 let left = left as usize;
91 let width = width as usize;
92 for y in top..top + height {
93 let start = y as usize * stride + left;
94 self.canvas[start..start + width].fill(self.dark_pixel);
95 }
96 }
97
98 fn into_image(self) -> String {
99 let w = self.width as usize;
100 let data = &self.canvas;
101 if data.is_empty() {
102 return String::new();
103 }
104 let row_group = $row_group;
105 let empty: &[u8] = &[];
106 let col_step: usize = $col_step;
107 let row_count = data.len() / w;
108 let mut out = String::with_capacity(self.output_capacity);
109
110 for group_start in (0..row_count).step_by(row_group) {
111 let actual = row_group.min(row_count - group_start);
112 let mut group: [&[u8]; $row_group] = [empty; $row_group];
113 for i in 0..actual {
114 let row_start = (group_start + i) * w;
115 group[i] = &data[row_start..row_start + w];
116 }
117 for col in (0..w).step_by(col_step) {
118 out.push_str($encode(&group, col));
119 }
120 if group_start + actual < row_count {
121 out.push('\n');
122 }
123 }
124 out
125 }
126 }
127 };
128}
129
130const CODEPAGE: [&str; 4] = [" ", "\u{2584}", "\u{2580}", "\u{2588}"];
134
135#[derive(Copy, Clone, PartialEq, Eq)]
138pub enum Dense1x2 {
139 Dark,
141 Light,
143}
144
145impl Pixel for Dense1x2 {
146 type Image = String;
147 type Canvas = Canvas1x2;
148 fn default_unit_size() -> (u32, u32) {
149 (1, 1)
150 }
151 fn default_color(color: Color) -> Dense1x2 {
152 color.select(Dense1x2::Dark, Dense1x2::Light)
153 }
154}
155
156impl Dense1x2 {
157 const fn value(self) -> u8 {
158 match self {
159 Dense1x2::Dark => 1,
160 Dense1x2::Light => 0,
161 }
162 }
163}
164
165fn encode_1x2(rows: &[&[u8]], col: usize) -> &'static str {
166 let top = rows[0].get(col).copied().unwrap_or(0);
167 let bot = rows[1].get(col).copied().unwrap_or(0);
168 CODEPAGE[usize::from(top * 2 + bot)]
169}
170
171impl_bit_canvas!(Canvas1x2, Dense1x2, 2, 1, 3, encode_1x2 as fn(&[&[u8]], usize) -> &'static str);
172
173const QUADRANT: [&str; 16] = [
179 " ", "\u{2598}", "\u{259D}", "\u{2580}", "\u{2596}", "\u{258C}", "\u{259E}", "\u{259B}", "\u{2597}", "\u{259A}", "\u{2590}", "\u{259C}", "\u{2584}", "\u{2599}", "\u{259F}", "\u{2588}", ];
196
197#[derive(Copy, Clone, PartialEq, Eq)]
200pub enum Dense2x2 {
201 Dark,
203 Light,
205}
206
207impl Pixel for Dense2x2 {
208 type Image = String;
209 type Canvas = Canvas2x2;
210 fn default_unit_size() -> (u32, u32) {
211 (1, 1)
212 }
213 fn default_color(color: Color) -> Dense2x2 {
214 color.select(Dense2x2::Dark, Dense2x2::Light)
215 }
216}
217
218impl Dense2x2 {
219 const fn value(self) -> u8 {
220 match self {
221 Dense2x2::Dark => 1,
222 Dense2x2::Light => 0,
223 }
224 }
225}
226
227fn encode_2x2(rows: &[&[u8]], col: usize) -> &'static str {
228 let tl = rows[0][col] & 1;
229 let tr = rows[0].get(col + 1).copied().unwrap_or(0) & 1;
230 let bl = rows[1].get(col).copied().unwrap_or(0) & 1;
231 let br = rows[1].get(col + 1).copied().unwrap_or(0) & 1;
232 QUADRANT[(tl | (tr << 1) | (bl << 2) | (br << 3)) as usize]
233}
234
235impl_bit_canvas!(Canvas2x2, Dense2x2, 2, 2, 3, encode_2x2 as fn(&[&[u8]], usize) -> &'static str);
236
237#[derive(Copy, Clone, PartialEq, Eq)]
258pub enum Braille {
259 Dark,
261 Light,
263}
264
265impl Pixel for Braille {
266 type Image = String;
267 type Canvas = CanvasBraille;
268 fn default_unit_size() -> (u32, u32) {
269 (1, 1)
270 }
271 fn default_color(color: Color) -> Braille {
272 color.select(Braille::Dark, Braille::Light)
273 }
274}
275
276impl Braille {
277 const fn value(self) -> u8 {
278 match self {
279 Braille::Dark => 1,
280 Braille::Light => 0,
281 }
282 }
283}
284
285const BRAILLE_UTF8: [[u8; 3]; 256] = {
287 let mut table = [[0u8; 3]; 256];
288 let mut i = 0usize;
289 while i < 256 {
290 let cp = 0x2800u32 + i as u32;
291 table[i][0] = ((cp >> 12) & 0x0F) as u8 | 0xE0;
292 table[i][1] = ((cp >> 6) & 0x3F) as u8 | 0x80;
293 table[i][2] = (cp & 0x3F) as u8 | 0x80;
294 i += 1;
295 }
296 table
297};
298
299fn encode_braille(rows: &[&[u8]], col: usize) -> &'static str {
300 let d1 = rows[0].get(col).copied().unwrap_or(0) & 1;
301 let d2 = rows[1].get(col).copied().unwrap_or(0) & 1;
302 let d3 = rows[2].get(col).copied().unwrap_or(0) & 1;
303 let d4 = rows[0].get(col + 1).copied().unwrap_or(0) & 1;
304 let d5 = rows[1].get(col + 1).copied().unwrap_or(0) & 1;
305 let d6 = rows[2].get(col + 1).copied().unwrap_or(0) & 1;
306 let d7 = rows[3].get(col).copied().unwrap_or(0) & 1;
307 let d8 = rows[3].get(col + 1).copied().unwrap_or(0) & 1;
308
309 let bits = d1 | (d2 << 1) | (d3 << 2) | (d4 << 3) | (d5 << 4) | (d6 << 5) | (d7 << 6) | (d8 << 7);
310 unsafe { core::str::from_utf8_unchecked(&BRAILLE_UTF8[bits as usize]) }
312}
313
314impl_bit_canvas!(CanvasBraille, Braille, 4, 2, 3, encode_braille as fn(&[&[u8]], usize) -> &'static str);
315
316#[derive(Copy, Clone, PartialEq, Eq)]
345pub enum Dense3x2 {
346 Dark,
348 Light,
350}
351
352impl Pixel for Dense3x2 {
353 type Image = String;
354 type Canvas = Canvas3x2;
355 fn default_unit_size() -> (u32, u32) {
356 (1, 1)
357 }
358 fn default_color(color: Color) -> Dense3x2 {
359 color.select(Dense3x2::Dark, Dense3x2::Light)
360 }
361}
362
363impl Dense3x2 {
364 const fn value(self) -> u8 {
365 match self {
366 Dense3x2::Dark => 1,
367 Dense3x2::Light => 0,
368 }
369 }
370}
371
372const SEXTANT_UTF8: [[u8; 4]; 64] = {
375 let mut table = [[0u8; 4]; 64];
376 let mut i = 0usize;
377 while i < 64 {
378 let cp = 0x1FB00u32 + i as u32;
379 table[i][0] = 0xF0u8 | ((cp >> 18) & 0x07) as u8;
380 table[i][1] = 0x80u8 | ((cp >> 12) & 0x3F) as u8;
381 table[i][2] = 0x80u8 | ((cp >> 6) & 0x3F) as u8;
382 table[i][3] = 0x80u8 | (cp & 0x3F) as u8;
383 i += 1;
384 }
385 table
386};
387
388fn encode_3x2(rows: &[&[u8]], col: usize) -> &'static str {
391 let d0 = rows[0].get(col).copied().unwrap_or(0) & 1;
392 let d1 = rows[1].get(col).copied().unwrap_or(0) & 1;
393 let d2 = rows[2].get(col).copied().unwrap_or(0) & 1;
394 let d3 = rows[0].get(col + 1).copied().unwrap_or(0) & 1;
395 let d4 = rows[1].get(col + 1).copied().unwrap_or(0) & 1;
396 let d5 = rows[2].get(col + 1).copied().unwrap_or(0) & 1;
397
398 let bits = d0 | (d1 << 1) | (d2 << 2) | (d3 << 3) | (d4 << 4) | (d5 << 5);
399 if bits == 0 {
400 " "
401 } else {
402 unsafe { core::str::from_utf8_unchecked(&SEXTANT_UTF8[bits as usize]) }
404 }
405}
406
407impl_bit_canvas!(Canvas3x2, Dense3x2, 3, 2, 4, encode_3x2 as fn(&[&[u8]], usize) -> &'static str);
408
409#[cfg(test)]
412mod budget_tests {
413 use super::*;
414 use crate::{MAX_BUFFER_BYTES, Renderer};
415
416 fn assert_safe_dimensions<P: Pixel<Image = String>>() {
417 let dark = P::default_color(Color::Dark);
418 let light = P::default_color(Color::Light);
419 for (width, height) in [(65_536, 65_536), (u32::MAX, u32::MAX)] {
420 assert_eq!(P::Canvas::validate_dimensions(width, height, &dark, &light), Err(RenderError::OutputTooLarge));
421 }
422 for (width, height) in [(0, 0), (0, u32::MAX), (u32::MAX, 0)] {
423 assert_eq!(P::Canvas::new(width, height, dark, light).into_image(), "");
424 }
425 let modules = [Color::Dark];
426 assert_eq!(
427 Renderer::<P>::new(&modules, 1, 0).module_dimensions(65_536, 65_536).try_build(),
428 Err(RenderError::OutputTooLarge)
429 );
430 }
431
432 #[test]
433 fn all_unicode_backends_guard_large_and_empty_dimensions() {
434 assert_safe_dimensions::<Dense1x2>();
435 assert_safe_dimensions::<Dense2x2>();
436 assert_safe_dimensions::<Braille>();
437 assert_safe_dimensions::<Dense3x2>();
438 }
439
440 fn assert_rectangles_match_pixels<P: Pixel<Image = String>>() {
441 for inverted in [false, true] {
442 let dark = P::default_color(if inverted { Color::Light } else { Color::Dark });
443 let light = P::default_color(if inverted { Color::Dark } else { Color::Light });
444 for (left, top, width, height) in [(0, 0, 1, 1), (1, 1, 3, 5), (6, 0, 1, 9), (0, 8, 7, 1), (0, 0, 7, 9)] {
445 let mut actual = P::Canvas::new(7, 9, dark, light);
446 let mut expected = P::Canvas::new(7, 9, dark, light);
447 actual.draw_dark_rect(left, top, width, height);
448 actual.draw_dark_rect(left, top, width, height);
449 for y in top..top + height {
450 for x in left..left + width {
451 expected.draw_dark_pixel(x, y);
452 }
453 }
454 assert_eq!(actual.into_image(), expected.into_image());
455 }
456 }
457 }
458
459 #[test]
460 fn unicode_rectangles_preserve_all_density_layouts_and_inverted_padding() {
461 assert_rectangles_match_pixels::<Dense1x2>();
462 assert_rectangles_match_pixels::<Dense2x2>();
463 assert_rectangles_match_pixels::<Braille>();
464 assert_rectangles_match_pixels::<Dense3x2>();
465 }
466
467 #[test]
468 #[should_panic(expected = "rectangle exceeds canvas dimensions")]
469 fn unicode_rectangle_cannot_alias_the_next_row() {
470 let mut canvas = Canvas1x2::new(3, 2, Dense1x2::Dark, Dense1x2::Light);
471 canvas.draw_dark_rect(3, 0, 2, 1);
472 }
473
474 #[test]
475 fn unicode_budget_counts_multibyte_output() {
476 let width = (MAX_BUFFER_BYTES / 4) as u32;
477 assert!(Canvas1x2::validate_dimensions(width, 1, &Dense1x2::Dark, &Dense1x2::Light).is_ok());
478 assert_eq!(
479 Canvas1x2::validate_dimensions(width + 1, 1, &Dense1x2::Dark, &Dense1x2::Light),
480 Err(RenderError::OutputTooLarge)
481 );
482 }
483
484 #[test]
485 fn odd_rows_preserve_padding_and_have_no_trailing_newline() {
486 let mut canvas = Canvas1x2::new(3, 3, Dense1x2::Dark, Dense1x2::Light);
487 canvas.draw_dark_pixel(0, 2);
488 assert_eq!(canvas.into_image(), " \n▀ ");
489 let modules = [Color::Dark; 9];
490 let output = Renderer::<Dense3x2>::new(&modules, 3, 0).build();
491 assert_eq!(output.len(), 8);
492 assert!(!output.ends_with('\n'));
493 }
494
495 #[test]
496 #[should_panic(expected = "rendered output exceeds coordinate or backend resource limits")]
497 fn direct_unicode_constructor_rejects_excessive_area() {
498 let _ = Canvas1x2::new(65_536, 65_536, Dense1x2::Dark, Dense1x2::Light);
499 }
500}
501
502#[test]
503fn test_render_to_utf8_string() {
504 use crate::Renderer;
505 let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark];
506 let image: String = Renderer::<Dense1x2>::new(colors, 2, 1).build();
507
508 assert_eq!(&image, " ▄ \n ▀ ");
509
510 let image2 = Renderer::<Dense1x2>::new(colors, 2, 1).module_dimensions(2, 2).build();
511
512 assert_eq!(&image2, " \n ██ \n ██ \n ");
513}
514
515#[test]
516fn integration_render_utf8_1x2() {
517 use crate::Renderer;
518 use crate::unicode::Dense1x2;
519
520 let colors = [Color::Dark, Color::Light, Color::Light, Color::Dark];
521 let image = Renderer::<Dense1x2>::new(&colors, 2, 0).module_dimensions(1, 1).build();
522 assert_eq!(image, "▀▄");
523}
524
525#[test]
526fn integration_render_utf8_1x2_inverted() {
527 use crate::Renderer;
528 use crate::unicode::Dense1x2;
529
530 let colors = [Color::Dark, Color::Light, Color::Light, Color::Dark];
531 let image = Renderer::<Dense1x2>::new(&colors, 2, 0)
532 .dark_color(Dense1x2::Light)
533 .light_color(Dense1x2::Dark)
534 .module_dimensions(1, 1)
535 .build();
536 assert_eq!(image, "▄▀");
537}
538
539#[test]
540fn test_dense2x2_basic() {
541 use crate::Renderer;
542 let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark];
543 let image: String = Renderer::<Dense2x2>::new(colors, 2, 0).module_dimensions(1, 1).build();
544 assert_eq!(&image, "\u{259A}");
545}
546
547#[test]
548fn test_dense2x2_with_quiet_zone() {
549 use crate::Renderer;
550 let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark];
551 let image: String = Renderer::<Dense2x2>::new(colors, 2, 1).build();
552 assert!(image.chars().count() >= 1);
553}
554
555#[test]
556fn test_dense2x2_all_dark() {
557 use crate::Renderer;
558 let colors = vec![Color::Dark; 4];
559 let image: String = Renderer::<Dense2x2>::new(&colors, 2, 0).module_dimensions(1, 1).build();
560 assert_eq!(&image, "\u{2588}");
561}
562
563#[test]
564fn test_dense2x2_all_light() {
565 use crate::Renderer;
566 let colors = vec![Color::Light; 4];
567 let image: String = Renderer::<Dense2x2>::new(&colors, 2, 0).module_dimensions(1, 1).build();
568 assert_eq!(&image, " ");
569}
570
571#[test]
572fn integration_render_utf8_2x2() {
573 use crate::Renderer;
574 use crate::unicode::Dense2x2;
575
576 let colors = vec![
577 Color::Dark,
578 Color::Light,
579 Color::Light,
580 Color::Dark,
581 Color::Light,
582 Color::Dark,
583 Color::Dark,
584 Color::Light,
585 Color::Dark,
586 Color::Dark,
587 Color::Light,
588 Color::Light,
589 Color::Light,
590 Color::Light,
591 Color::Dark,
592 Color::Dark,
593 ];
594 let image = Renderer::<Dense2x2>::new(&colors, 4, 0).module_dimensions(1, 1).build();
595 assert!(!image.is_empty());
596 let dense1x2 = Renderer::<Dense1x2>::new(&colors, 4, 0).module_dimensions(1, 1).build();
597 assert!(image.len() < dense1x2.len());
598}
599
600#[test]
601fn test_braille_all_dark() {
602 use crate::Renderer;
603 let colors = vec![Color::Dark; 16];
604 let image: String = Renderer::<Braille>::new(&colors, 4, 0).module_dimensions(1, 1).build();
605 assert_eq!(&image, "\u{28FF}\u{28FF}");
606}
607
608#[test]
609fn test_braille_all_light() {
610 use crate::Renderer;
611 let colors = vec![Color::Light; 16];
612 let image: String = Renderer::<Braille>::new(&colors, 4, 0).module_dimensions(1, 1).build();
613 assert_eq!(&image, "\u{2800}\u{2800}");
614}
615
616#[test]
617fn test_braille_top_left_dot() {
618 use crate::Renderer;
619 let mut colors = vec![Color::Light; 16];
620 colors[0] = Color::Dark;
621 let image: String = Renderer::<Braille>::new(&colors, 4, 0).module_dimensions(1, 1).build();
622 assert_eq!(&image, "\u{2801}\u{2800}");
623}
624
625#[test]
626fn test_braille_density() {
627 use crate::Renderer;
628 use crate::unicode::{Braille, Dense1x2};
629
630 let colors = (0..64).map(|i| if i % 3 == 0 { Color::Dark } else { Color::Light }).collect::<Vec<_>>();
631 let braille = Renderer::<Braille>::new(&colors, 8, 0).module_dimensions(1, 1).build();
632 let dense1x2 = Renderer::<Dense1x2>::new(&colors, 8, 0).module_dimensions(1, 1).build();
633 assert!(braille.len() < dense1x2.len());
634}
635
636#[test]
637fn test_dense3x2_all_dark() {
638 use crate::Renderer;
639 let colors = vec![Color::Dark; 36];
641 let image: String = Renderer::<Dense3x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
642 assert_eq!(&image, "\u{1FB3F}\u{1FB3F}\u{1FB3F}\n\u{1FB3F}\u{1FB3F}\u{1FB3F}");
643}
644
645#[test]
646fn test_dense3x2_all_light() {
647 use crate::Renderer;
648 let colors = vec![Color::Light; 36];
649 let image: String = Renderer::<Dense3x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
650 assert_eq!(&image, " \n ");
651}
652
653#[test]
654fn test_dense3x2_top_left_cell() {
655 use crate::Renderer;
656 let mut colors = vec![Color::Light; 36];
658 colors[0] = Color::Dark;
659 let image: String = Renderer::<Dense3x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
660 assert!(image.starts_with('\u{1FB01}'));
662}
663
664#[test]
665fn test_dense3x2_density() {
666 use crate::Renderer;
667 use crate::unicode::{Dense1x2, Dense3x2};
668
669 let colors = (0..36).map(|i| if i % 2 == 0 { Color::Dark } else { Color::Light }).collect::<Vec<_>>();
670 let sextant = Renderer::<Dense3x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
671 let dense1x2 = Renderer::<Dense1x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
672 assert!(sextant.len() < dense1x2.len());
674}