1use std::io::Write;
2use std::path::Path;
3
4use crate::{Error, Result};
5
6pub type Color = [u8; 3];
7
8pub const BG: Color = [16, 19, 25];
9pub const TEXT: Color = [232, 235, 240];
10pub const DIM: Color = [138, 146, 160];
11pub const GRID: Color = [44, 50, 62];
12pub const SEP: Color = [70, 78, 92];
13pub const OURS: Color = [220, 110, 34];
14pub const THEIRS: Color = [74, 142, 230];
15pub const FASTER: Color = [74, 201, 125];
16pub const SLOWER: Color = [235, 88, 88];
17
18const GLYPHS: &[(char, [u8; 7])] = &[
19 ('A', [14, 17, 17, 31, 17, 17, 17]),
20 ('B', [30, 17, 17, 30, 17, 17, 30]),
21 ('C', [14, 17, 16, 16, 16, 17, 14]),
22 ('D', [30, 17, 17, 17, 17, 17, 30]),
23 ('E', [31, 16, 16, 30, 16, 16, 31]),
24 ('F', [31, 16, 16, 30, 16, 16, 16]),
25 ('G', [14, 17, 16, 23, 17, 17, 15]),
26 ('H', [17, 17, 17, 31, 17, 17, 17]),
27 ('I', [14, 4, 4, 4, 4, 4, 14]),
28 ('J', [7, 2, 2, 2, 2, 18, 12]),
29 ('K', [17, 18, 20, 24, 20, 18, 17]),
30 ('L', [16, 16, 16, 16, 16, 16, 31]),
31 ('M', [17, 27, 21, 21, 17, 17, 17]),
32 ('N', [17, 25, 21, 19, 17, 17, 17]),
33 ('O', [14, 17, 17, 17, 17, 17, 14]),
34 ('P', [30, 17, 17, 30, 16, 16, 16]),
35 ('Q', [14, 17, 17, 17, 21, 18, 13]),
36 ('R', [30, 17, 17, 30, 20, 18, 17]),
37 ('S', [15, 16, 16, 14, 1, 1, 30]),
38 ('T', [31, 4, 4, 4, 4, 4, 4]),
39 ('U', [17, 17, 17, 17, 17, 17, 14]),
40 ('V', [17, 17, 17, 17, 17, 10, 4]),
41 ('W', [17, 17, 17, 21, 21, 21, 10]),
42 ('X', [17, 17, 10, 4, 10, 17, 17]),
43 ('Y', [17, 17, 10, 4, 4, 4, 4]),
44 ('Z', [31, 1, 2, 4, 8, 16, 31]),
45 ('0', [14, 17, 19, 21, 25, 17, 14]),
46 ('1', [4, 12, 4, 4, 4, 4, 14]),
47 ('2', [14, 17, 1, 2, 4, 8, 31]),
48 ('3', [30, 1, 1, 14, 1, 1, 30]),
49 ('4', [2, 6, 10, 18, 31, 2, 2]),
50 ('5', [31, 16, 30, 1, 1, 17, 14]),
51 ('6', [6, 8, 16, 30, 17, 17, 14]),
52 ('7', [31, 1, 2, 4, 8, 8, 8]),
53 ('8', [14, 17, 17, 14, 17, 17, 14]),
54 ('9', [14, 17, 17, 15, 1, 2, 12]),
55 ('.', [0, 0, 0, 0, 0, 12, 12]),
56 (',', [0, 0, 0, 0, 12, 4, 8]),
57 (':', [0, 12, 12, 0, 12, 12, 0]),
58 ('-', [0, 0, 0, 31, 0, 0, 0]),
59 ('+', [0, 4, 4, 31, 4, 4, 0]),
60 ('/', [1, 1, 2, 4, 8, 16, 16]),
61 ('(', [2, 4, 8, 8, 8, 4, 2]),
62 (')', [8, 4, 2, 2, 2, 4, 8]),
63 ('%', [25, 26, 2, 4, 8, 11, 19]),
64 ('=', [0, 0, 31, 0, 31, 0, 0]),
65 ('<', [2, 4, 8, 16, 8, 4, 2]),
66 ('>', [8, 4, 2, 1, 2, 4, 8]),
67 ('\'', [4, 4, 8, 0, 0, 0, 0]),
68 ('_', [0, 0, 0, 0, 0, 0, 31]),
69 ('×', [0, 17, 10, 4, 10, 17, 0]),
70 ('*', [0, 21, 14, 31, 14, 21, 0]),
71 ('†', [4, 14, 4, 4, 4, 4, 0]),
72 (';', [0, 12, 12, 0, 12, 4, 8]),
73 ('|', [4, 4, 4, 4, 4, 4, 4]),
74 (' ', [0, 0, 0, 0, 0, 0, 0]),
75];
76
77fn find(c: char) -> Option<[u8; 7]> {
78 let c = c.to_ascii_uppercase();
79 GLYPHS.iter().find(|(g, _)| *g == c).map(|(_, rows)| *rows)
80}
81
82pub fn has_glyph(c: char) -> bool {
83 find(c).is_some()
84}
85
86fn glyph(c: char) -> [u8; 7] {
87 find(c).unwrap_or([0; 7])
88}
89
90pub struct Canvas {
91 width: usize,
92 height: usize,
93 px: Vec<u8>,
94}
95
96impl Canvas {
97 pub fn new(width: usize, height: usize) -> Self {
98 let px = BG.repeat(width * height);
99 Self { width, height, px }
100 }
101
102 pub fn width(&self) -> usize {
103 self.width
104 }
105
106 pub fn height(&self) -> usize {
107 self.height
108 }
109
110 pub fn pixel(&self, x: usize, y: usize) -> Option<Color> {
111 if x >= self.width || y >= self.height {
112 return None;
113 }
114 let at = (y * self.width + x) * 3;
115 Some([self.px[at], self.px[at + 1], self.px[at + 2]])
116 }
117
118 pub fn rect(&mut self, x: i64, y: i64, w: i64, h: i64, c: Color) {
119 let x0 = x.clamp(0, self.width as i64) as usize;
120 let x1 = (x + w).clamp(0, self.width as i64) as usize;
121 let y0 = y.clamp(0, self.height as i64) as usize;
122 let y1 = (y + h).clamp(0, self.height as i64) as usize;
123 for row in y0..y1 {
124 for col in x0..x1 {
125 let at = (row * self.width + col) * 3;
126 self.px[at..at + 3].copy_from_slice(&c);
127 }
128 }
129 }
130
131 pub fn text_width(text: &str, scale: usize) -> i64 {
132 (text.chars().count() * 6 * scale) as i64 - scale as i64
133 }
134
135 pub fn text(&mut self, x: i64, y: i64, text: &str, scale: usize, c: Color) {
136 let s = scale as i64;
137 let mut pen = x;
138 for ch in text.chars() {
139 let rows = glyph(ch);
140 for (r, bits) in rows.iter().enumerate() {
141 for col in 0..5 {
142 if bits >> (4 - col) & 1 == 1 {
143 self.rect(pen + col as i64 * s, y + r as i64 * s, s, s, c);
144 }
145 }
146 }
147 pen += 6 * s;
148 }
149 }
150
151 pub fn write_png<W: Write>(&self, out: W) -> Result<()> {
152 let mut enc = png::Encoder::new(out, self.width as u32, self.height as u32);
153 enc.set_color(png::ColorType::Rgb);
154 enc.set_depth(png::BitDepth::Eight);
155 let mut w = enc
156 .write_header()
157 .map_err(|e| Error::new(format!("cannot write the PNG: {e}")))?;
158 w.write_image_data(&self.px)
159 .map_err(|e| Error::new(format!("cannot write the PNG: {e}")))?;
160 w.finish()
161 .map_err(|e| Error::new(format!("cannot write the PNG: {e}")))
162 }
163
164 pub fn png(&self) -> Result<Vec<u8>> {
165 let mut out = Vec::new();
166 self.write_png(&mut out)?;
167 Ok(out)
168 }
169
170 pub fn save(&self, path: &Path) -> Result<()> {
171 if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
172 std::fs::create_dir_all(dir)
173 .map_err(|e| Error::new(format!("cannot create {}: {e}", dir.display())))?;
174 }
175 let bytes = self.png()?;
176 if std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_symlink()) {
177 return Err(Error::new(format!(
178 "{} is a symlink; not writing through it",
179 path.display()
180 )));
181 }
182 std::fs::write(path, bytes)
183 .map_err(|e| Error::new(format!("cannot write {}: {e}", path.display())))
184 }
185}