Skip to main content

pito_compare/
canvas.rs

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}