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 missingxoryis 0);<path>ofM,L,H,Vandz(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", andfill-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.
An icon as the pixels it fills, later shapes over earlier.
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.
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.
#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.
An edge from a to b, which must be along a row or a column.
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}