raster.rsannotatedraster.rssource295 lines · 12.0 KB · raw
1//! Coloured pixel art read from the pack's SVG files.
2//!
3//! The pack's icons are grids of 9 by 9 squares. This reads exactly what they use and refuses the rest, so a
4//! file that cannot be drawn faithfully is an error rather than a wrong picture:
5//!
6//! - `<rect>` (a missing `x` or `y` is 0);
7//! - `<path>` of `M`, `L`, `H`, `V` and `z` (absolute or relative) along rows and columns, and curves that are
8//!   in fact straight;
9//! - `<polygon>` and `<polyline>` of points that step only along a row or a column (a polyline is filled as
10//!   closed, which is what SVG does);
11//! - `fill="#rrggbb"`, and `fill-rule="evenodd"` (the default is non-zero).
12//!
13//! Every edge is therefore axis-aligned, and a pixel is in or out by its centre. Ported from the card
14//! rasteriser of `~/lmjtfy` (rects and `M`/`H`/`V`/`z` paths), plus polygons, polylines, `evenodd` and a
15//! `viewBox` that does not start at the origin, which the pack needs.
16
17/// One filled pixel: its column and row, and its colour.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct Cell {
20    pub x: usize,
21    pub y: usize,
22    pub rgb: [u8; 3],
23}
24
25/// An icon as the pixels it fills, later shapes over earlier.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct Icon {
28    /// The side of its square grid.
29    pub size: usize,
30    pub cells: Vec<Cell>,
31}
32
33impl Icon {
34    /// The icon in `svg`, or why it cannot be read.
35    pub fn parse(svg: &str) -> Result<Icon, String> {
36        let view: Vec<f64> = attribute(svg, "viewBox")
37            .map(|v| v.split_whitespace().filter_map(|n| n.parse().ok()).collect())
38            .unwrap_or_default();
39        let [ox, oy, w, h] = view[..] else { return Err("no viewBox of four numbers".into()) };
40        if w != h || w < 1.0 || w.fract() != 0.0 {
41            return Err("the viewBox is not a square of whole pixels".into());
42        }
43        let size = w as usize;
44        let mut grid: Vec<Option<[u8; 3]>> = vec![None; size * size];
45        let mut rest = svg;
46        while let Some(open) = rest.find('<') {
47            rest = &rest[open + 1..];
48            let end = rest.find('>').ok_or("an unclosed tag")?;
49            let tag = &rest[..end];
50            rest = &rest[end..];
51            let name = tag.split_whitespace().next().unwrap_or_default();
52            if !matches!(name, "rect" | "path" | "polygon" | "polyline") {
53                if !matches!(name, "svg" | "/svg" | "") && !name.starts_with('/') {
54                    return Err(format!("the element <{name}> is not one this reads"));
55                }
56                continue;
57            }
58            let rgb = colour(attribute(tag, "fill").ok_or("a shape without a fill")?)?;
59            let evenodd = match attribute(tag, "fill-rule") {
60                None | Some("nonzero") => false,
61                Some("evenodd") => true,
62                Some(other) => return Err(format!("the fill-rule {other} is not nonzero or evenodd")),
63            };
64            let covers: Box<dyn Fn(f64, f64) -> bool> = if name == "rect" {
65                let number = |key: &str| match attribute(tag, key) {
66                    None if key == "x" || key == "y" => Ok(0.0),
67                    None => Err(format!("a rect without {key}")),
68                    Some(v) => v.parse::<f64>().map_err(|_| format!("a rect with {key}={v}")),
69                };
70                let (x, y, w, h) = (number("x")?, number("y")?, number("width")?, number("height")?);
71                Box::new(move |cx, cy| cx > x && cx < x + w && cy > y && cy < y + h)
72            } else {
73                let edges = if name == "path" {
74                    edges_of_path(attribute(tag, "d").ok_or("a path without d")?)?
75                } else {
76                    edges_of_points(attribute(tag, "points").ok_or("a polygon without points")?)?
77                };
78                Box::new(move |cx, cy| inside(&edges, cx, cy, evenodd))
79            };
80            for row in 0..size {
81                for column in 0..size {
82                    if covers(ox + column as f64 + 0.5, oy + row as f64 + 0.5) {
83                        grid[row * size + column] = Some(rgb);
84                    }
85                }
86            }
87        }
88        let cells: Vec<Cell> = grid.iter().enumerate().filter_map(|(at, rgb)| rgb.map(|rgb| Cell { x: at % size, y: at / size, rgb })).collect();
89        if cells.is_empty() {
90            return Err("nothing is drawn".into());
91        }
92        Ok(Icon { size, cells })
93    }
94
95    /// Every colour it uses, in the order first met.
96    pub fn colours(&self) -> Vec<[u8; 3]> {
97        let mut seen: Vec<[u8; 3]> = Vec::new();
98        for cell in &self.cells {
99            if !seen.contains(&cell.rgb) {
100                seen.push(cell.rgb);
101            }
102        }
103        seen
104    }
105
106    /// One line per row: `.` for empty, otherwise the colour's index among [`colours`](Self::colours) as a digit or
107    /// a lowercase letter. For tests and for a reader to look at.
108    pub fn ascii(&self) -> String {
109        let colours = self.colours();
110        let mut rows = vec![vec!['.'; self.size]; self.size];
111        for c in &self.cells {
112            let i = colours.iter().position(|x| *x == c.rgb).expect("a cell's colour is listed");
113            rows[c.y][c.x] = char::from_digit(i as u32, 36).unwrap_or('?');
114        }
115        rows.into_iter().map(|r| r.into_iter().collect::<String>()).collect::<Vec<_>>().join("\n")
116    }
117}
118
119/// The value of `key="..."` in `text`.
120fn attribute<'a>(text: &'a str, key: &str) -> Option<&'a str> {
121    let at = text.find(&format!(" {key}=\""))? + key.len() + 3;
122    text[at..].split('"').next()
123}
124
125/// `#rrggbb`.
126fn colour(text: &str) -> Result<[u8; 3], String> {
127    let hex = text.strip_prefix('#').filter(|hex| hex.len() == 6).ok_or_else(|| format!("fill {text} is not #rrggbb"))?;
128    let byte = |at: usize| u8::from_str_radix(&hex[at..at + 2], 16).map_err(|_| format!("fill {text} is not #rrggbb"));
129    Ok([byte(0)?, byte(2)?, byte(4)?])
130}
131
132/// A vertical edge: `x`, from `from` to `to`. Only these cross a horizontal ray.
133type Edge = (f64, f64, f64);
134
135/// Closes a subpath from `at` back to `start`, which must be along a row or a column.
136fn close(edges: &mut Vec<Edge>, at: (f64, f64), start: (f64, f64)) -> Result<(), String> {
137    step(edges, at, start)
138}
139
140/// An edge from `a` to `b`, which must be along a row or a column.
141fn step(edges: &mut Vec<Edge>, a: (f64, f64), b: (f64, f64)) -> Result<(), String> {
142    if a.0 != b.0 && a.1 != b.1 {
143        return Err("an edge runs diagonally".into());
144    }
145    if a.0 == b.0 && a.1 != b.1 {
146        edges.push((a.0, a.1, b.1));
147    }
148    Ok(())
149}
150
151fn edges_of_points(points: &str) -> Result<Vec<Edge>, String> {
152    let numbers: Vec<f64> = points
153        .split(|c: char| c.is_whitespace() || c == ',')
154        .filter(|t| !t.is_empty())
155        .map(|t| t.parse::<f64>().map_err(|_| format!("{t} is not a number")))
156        .collect::<Result<_, _>>()?;
157    if numbers.len() % 2 != 0 || numbers.len() < 6 {
158        return Err("a polygon needs at least three whole points".into());
159    }
160    let pts: Vec<(f64, f64)> = numbers.chunks(2).map(|c| (c[0], c[1])).collect();
161    let mut edges = Vec::new();
162    for pair in pts.windows(2) {
163        step(&mut edges, pair[0], pair[1])?;
164    }
165    close(&mut edges, pts[pts.len() - 1], pts[0])?;
166    Ok(edges)
167}
168
169/// The numbers that follow a path command: a sign starts a number, so `0-0.9` is two.
170fn path_numbers(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Result<Vec<f64>, String> {
171    let mut numbers = Vec::new();
172    loop {
173        while chars.peek().is_some_and(|c| c.is_whitespace() || *c == ',') {
174            chars.next();
175        }
176        let mut text = String::new();
177        if chars.peek() == Some(&'-') {
178            text.push(chars.next().expect("peeked"));
179        }
180        let mut dot = false;
181        while let Some(&c) = chars.peek() {
182            if c.is_ascii_digit() || (c == '.' && !dot) {
183                dot |= c == '.';
184                text.push(c);
185                chars.next();
186            } else {
187                break;
188            }
189        }
190        if text.is_empty() {
191            return Ok(numbers);
192        }
193        numbers.push(text.parse::<f64>().map_err(|_| format!("{text} is not a number"))?);
194    }
195}
196
197/// The vertical edges of a path of `M`/`m`, `L`/`l`, `H`/`h`, `V`/`v`, `z`, and `C`/`c` curves whose
198/// control points lie on the straight line between their ends (the pack has a few, drawn that way by its
199/// optimiser); any other curve is refused.
200fn edges_of_path(d: &str) -> Result<Vec<Edge>, String> {
201    let mut edges = Vec::new();
202    let (mut start, mut at) = ((0.0, 0.0), (0.0, 0.0));
203    let mut chars = d.chars().peekable();
204    let mut open = false;
205    while let Some(command) = chars.next() {
206        if command.is_whitespace() || command == ',' {
207            continue;
208        }
209        let numbers = path_numbers(&mut chars)?;
210        let rel = command.is_ascii_lowercase();
211        let (bx, by) = if rel { at } else { (0.0, 0.0) };
212        let group = |n: usize| -> Result<std::slice::Chunks<'_, f64>, String> {
213            if numbers.is_empty() || numbers.len() % n != 0 {
214                Err(format!("the path command {command} has {} numbers", numbers.len()))
215            } else {
216                Ok(numbers.chunks(n))
217            }
218        };
219        match command.to_ascii_uppercase() {
220            'M' => {
221                let mut pairs = group(2)?;
222                let first = pairs.next().expect("a group");
223                if open {
224                    close(&mut edges, at, start)?;
225                }
226                start = (bx + first[0], by + first[1]);
227                at = start;
228                open = true;
229                for p in pairs {
230                    let to = (if rel { at.0 } else { 0.0 } + p[0], if rel { at.1 } else { 0.0 } + p[1]);
231                    step(&mut edges, at, to)?;
232                    at = to;
233                }
234            }
235            'L' => {
236                for p in group(2)? {
237                    let to = (if rel { at.0 } else { 0.0 } + p[0], if rel { at.1 } else { 0.0 } + p[1]);
238                    step(&mut edges, at, to)?;
239                    at = to;
240                }
241            }
242            'H' => {
243                for x in group(1)? {
244                    let to = (if rel { at.0 } else { 0.0 } + x[0], at.1);
245                    step(&mut edges, at, to)?;
246                    at = to;
247                }
248            }
249            'V' => {
250                for y in group(1)? {
251                    let to = (at.0, if rel { at.1 } else { 0.0 } + y[0]);
252                    step(&mut edges, at, to)?;
253                    at = to;
254                }
255            }
256            'C' => {
257                for c in group(6)? {
258                    let o = if rel { at } else { (0.0, 0.0) };
259                    let (p1, p2, to) = ((o.0 + c[0], o.1 + c[1]), (o.0 + c[2], o.1 + c[3]), (o.0 + c[4], o.1 + c[5]));
260                    let on_column = [p1, p2, to].iter().all(|p| p.0 == at.0);
261                    let on_row = [p1, p2, to].iter().all(|p| p.1 == at.1);
262                    if !on_column && !on_row {
263                        return Err("a curve that is not a straight line".into());
264                    }
265                    step(&mut edges, at, to)?;
266                    at = to;
267                }
268            }
269            'Z' if numbers.is_empty() => {
270                close(&mut edges, at, start)?;
271                at = start;
272                open = false;
273            }
274            _ => return Err(format!("the path command {command} is not M, L, H, V, C or z")),
275        }
276    }
277    if open {
278        close(&mut edges, at, start)?;
279    }
280    Ok(edges)
281}
282
283/// Whether `(px, py)` is inside: the edges to its right that cross its row, up counting one way and down the
284/// other; non-zero (SVG's default) or odd (`evenodd`).
285fn inside(edges: &[Edge], px: f64, py: f64, evenodd: bool) -> bool {
286    let (mut winding, mut crossings) = (0i32, 0u32);
287    for (x, from, to) in edges {
288        let (low, high) = (from.min(*to), from.max(*to));
289        if *x > px && py > low && py < high {
290            winding += if to > from { 1 } else { -1 };
291            crossings += 1;
292        }
293    }
294    if evenodd { crossings % 2 == 1 } else { winding != 0 }
295}