1use crate::canvas::Canvas;
4use crate::{Error, Limits, Result, Table, check_size};
5use font8x8::{BASIC_FONTS, BLOCK_FONTS, BOX_FONTS, GREEK_FONTS, HIRAGANA_FONTS, LATIN_FONTS, UnicodeFonts};
6
7const GLYPH: usize = 8;
8const PAD: usize = 4;
9const ROW: usize = GLYPH + 2 * PAD;
10const INK: [u8; 3] = [20, 20, 20];
11const GRID: [u8; 3] = [200, 200, 200];
12const HEAD: [u8; 3] = [235, 235, 235];
13
14pub(crate) fn glyph(c: char) -> [u8; 8] {
16 let c = if c == '\t' { ' ' } else { c };
17 if c.is_control() {
18 return BASIC_FONTS.get('?').unwrap_or_default();
19 }
20 BASIC_FONTS
21 .get(c)
22 .or_else(|| LATIN_FONTS.get(c))
23 .or_else(|| GREEK_FONTS.get(c))
24 .or_else(|| BOX_FONTS.get(c))
25 .or_else(|| BLOCK_FONTS.get(c))
26 .or_else(|| HIRAGANA_FONTS.get(c))
27 .unwrap_or_else(|| BASIC_FONTS.get('?').unwrap_or_default())
28}
29
30pub(crate) fn cell_text(s: &str, max: usize) -> Vec<char> {
32 let mut c: Vec<char> = s.chars().map(|c| if c == '\n' || c == '\r' { ' ' } else { c }).collect();
33 if c.len() > max {
34 c.truncate(max.saturating_sub(1));
35 if max > 0 {
36 c.push('~');
37 }
38 }
39 c
40}
41
42fn numeric(s: &str) -> bool {
43 let t = s.trim();
44 !t.is_empty() && t.parse::<f64>().is_ok()
45}
46
47fn check(t: &Table, l: &Limits) -> Result<()> {
48 if t.columns.is_empty() {
49 return Err(Error::Invalid("a table needs at least one column".into()));
50 }
51 if t.columns.len() > l.max_columns {
52 return Err(Error::TooLarge(format!("{} columns (at most {})", t.columns.len(), l.max_columns)));
53 }
54 if t.rows.len() > l.max_rows {
55 return Err(Error::TooLarge(format!("{} rows (at most {})", t.rows.len(), l.max_rows)));
56 }
57 if let Some(i) = t.rows.iter().position(|r| r.len() != t.columns.len()) {
58 return Err(Error::Invalid(format!("row {} has {} cells for {} columns", i + 1, t.rows[i].len(), t.columns.len())));
59 }
60 Ok(())
61}
62
63pub fn table_png(t: &Table, limits: &Limits) -> Result<Vec<u8>> {
65 check(t, limits)?;
66 let head: Vec<Vec<char>> = t.columns.iter().map(|c| cell_text(c, limits.max_cell_chars)).collect();
67 let body: Vec<Vec<Vec<char>>> = t.rows.iter().map(|r| r.iter().map(|c| cell_text(c, limits.max_cell_chars)).collect()).collect();
68 let widths: Vec<usize> =
69 (0..head.len()).map(|j| body.iter().map(|r| r[j].len()).chain([head[j].len()]).max().unwrap_or(0) * GLYPH + 2 * PAD).collect();
70 let w = widths.iter().sum::<usize>() + 1;
71 let h = (body.len() + 1) * ROW + 1;
72 check_size(w, h, limits)?;
73 let mut c = Canvas::new(w, h, [255; 3]);
74 c.fill(0, 0, w, ROW, HEAD);
75 let mut y = 0;
76 for (r, cells) in std::iter::once(&head).chain(body.iter()).enumerate() {
77 let mut x = 0;
78 for (j, text) in cells.iter().enumerate() {
79 let right = r > 0 && numeric(&t.rows[r - 1][j]);
80 let start = if right { x + widths[j] - PAD - text.len() * GLYPH } else { x + PAD };
81 for (k, ch) in text.iter().enumerate() {
82 c.glyph(start + k * GLYPH, y + PAD, glyph(*ch), INK);
83 }
84 x += widths[j];
85 }
86 y += ROW;
87 }
88 for r in 0..=body.len() + 1 {
89 c.hline(0, w - 1, r * ROW, GRID);
90 }
91 let mut x = 0;
92 for wd in std::iter::once(0).chain(widths.iter().copied()) {
93 x += wd;
94 c.vline(x, 0, h - 1, GRID);
95 }
96 c.png()
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn decode(b: &[u8]) -> (usize, usize, Vec<u8>) {
104 let mut r = ::png::Decoder::new(std::io::Cursor::new(b)).read_info().unwrap();
105 let mut buf = vec![0; r.output_buffer_size().unwrap()];
106 let i = r.next_frame(&mut buf).unwrap();
107 buf.truncate(i.buffer_size());
108 (i.width as usize, i.height as usize, buf)
109 }
110
111 fn t(cols: &[&str], rows: &[&[&str]]) -> Table {
112 Table {
113 columns: cols.iter().map(|s| s.to_string()).collect(),
114 rows: rows.iter().map(|r| r.iter().map(|s| s.to_string()).collect()).collect(),
115 }
116 }
117
118 #[test]
119 fn a_table_is_a_grid_of_text() {
120 let tb = t(&["name", "temp"], &[&["north", "21.5"], &["south", "-3"]]);
121 let (w, h, px) = decode(&table_png(&tb, &Limits::default()).unwrap());
122 assert_eq!((w, h), (5 * 8 + 8 + 4 * 8 + 8 + 1, 3 * 16 + 1));
124 let at = |x: usize, y: usize| px[(y * w + x) * 3..][..3].to_vec();
125 assert_eq!(at(0, 0), GRID.to_vec());
126 assert_eq!(at(2, 2), HEAD.to_vec(), "header background");
127 assert_eq!(at(2, 18), vec![255; 3], "body background");
128 let ink =
129 |x0: usize, y0: usize| (y0..y0 + 8).flat_map(|y| (x0..x0 + 8).map(move |x| (x, y))).filter(|&(x, y)| at(x, y) == INK.to_vec()).count();
130 assert!(ink(4, 4) > 0);
132 let col2 = 48;
133 assert_eq!(ink(col2 + 4, 36), 0, "the number sits at the right, not the left");
134 assert!(ink(col2 + 40 - 4 - 8, 36) > 0, "its last digit ends at the right padding");
135 }
136
137 #[test]
138 fn same_table_same_bytes_and_text_matters() {
139 let l = Limits::default();
140 let a = table_png(&t(&["a"], &[&["1"]]), &l).unwrap();
141 assert_eq!(a, table_png(&t(&["a"], &[&["1"]]), &l).unwrap());
142 assert_ne!(a, table_png(&t(&["a"], &[&["2"]]), &l).unwrap());
143 assert_eq!(&a[1..4], b"PNG");
144 }
145
146 #[test]
147 fn malformed_tables_and_limits_are_errors() {
148 let l = Limits::default();
149 assert!(matches!(table_png(&Table::default(), &l), Err(Error::Invalid(_))));
150 assert_eq!(table_png(&t(&["a", "b"], &[&["1"]]), &l), Err(Error::Invalid("row 1 has 1 cells for 2 columns".into())));
151 assert!(matches!(table_png(&t(&["a", "b"], &[]), &Limits { max_columns: 1, ..l }), Err(Error::TooLarge(_))));
152 assert!(matches!(table_png(&t(&["a"], &[&["1"], &["2"]]), &Limits { max_rows: 1, ..l }), Err(Error::TooLarge(_))));
153 assert!(matches!(table_png(&t(&["a"], &[&["1"]]), &Limits { max_pixels: 10, ..l }), Err(Error::TooLarge(_))));
154 assert!(matches!(table_png(&t(&["a"], &[&["1"]]), &Limits { max_height: 20, ..l }), Err(Error::TooLarge(_))));
155 let long = "x".repeat(100_000);
157 let (w, _, _) = decode(&table_png(&t(&["a"], &[&[long.as_str()]]), &l).unwrap());
158 assert_eq!(w, 80 * 8 + 8 + 1);
159 }
160
161 #[test]
162 fn cells_are_cut_and_unknown_characters_shown() {
163 assert_eq!(cell_text("abcdef", 4).iter().collect::<String>(), "abc~");
164 assert_eq!(cell_text("a\nb", 9).iter().collect::<String>(), "a b");
165 assert_eq!(cell_text("abc", 0), Vec::<char>::new());
166 assert_eq!(glyph('\u{7}'), glyph('?'));
167 assert_eq!(glyph('\u{1F600}'), glyph('?'));
168 assert_ne!(glyph('é'), glyph('?'));
169 assert_ne!(glyph('A'), glyph('?'));
170 }
171}