raster.rsannotatedraster.rssource295 lines · 12.0 KB · raw

Coloured pixel art read from the pack's SVG files.

The pack's icons are grids of 9 by 9 squares. This reads exactly what they use and refuses the rest, so a file that cannot be drawn faithfully is an error rather than a wrong picture:

  • <rect> (a missing x or y is 0);
  • <path> of M, L, H, V and z (absolute or relative) along rows and columns, and curves that are in fact straight;
  • <polygon> and <polyline> of points that step only along a row or a column (a polyline is filled as closed, which is what SVG does);
  • fill="#rrggbb", and fill-rule="evenodd" (the default is non-zero).

Every edge is therefore axis-aligned, and a pixel is in or out by its centre. Ported from the card rasteriser of ~/lmjtfy (rects and M/H/V/z paths), plus polygons, polylines, evenodd and a viewBox that does not start at the origin, which the pack needs.

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}

An icon as the pixels it fills, later shapes over earlier.

26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct Icon {

The side of its square grid.

29    pub size: usize,
30    pub cells: Vec<Cell>,
31}
33impl Icon {

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    }

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    }

One line per row: . for empty, otherwise the colour's index among colours as a digit or 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}

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}

#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}

A vertical edge: x, from from to to. Only these cross a horizontal ray.

133type Edge = (f64, f64, f64);

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}

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}
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}

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}

The vertical edges of a path of M/m, L/l, H/h, V/v, z, and C/c curves whose control points lie on the straight line between their ends (the pack has a few, drawn that way by its 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}

Whether (px, py) is inside: the edges to its right that cross its row, up counting one way and down the 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}