use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
fn main() {
let glyph_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/glyphs");
println!("cargo:rerun-if-changed={}", glyph_dir.display());
println!("cargo:rerun-if-changed=build.rs");
let mut icons: BTreeMap<u32, Icon> = BTreeMap::new();
let entries = std::fs::read_dir(&glyph_dir)
.unwrap_or_else(|e| panic!("read {}: {e}", glyph_dir.display()));
for entry in entries {
let path = entry.expect("read glyph dir entry").path();
if path.extension().and_then(|e| e.to_str()) != Some("svg") {
continue;
}
println!("cargo:rerun-if-changed={}", path.display());
if let Some(icon) = parse(&path) {
if let Some(prev) = icons.insert(icon.cp, icon) {
panic!("two glyph SVGs claim U+{:04X} (one is {})", prev.cp, prev.name);
}
}
}
assert!(!icons.is_empty(), "no glyph SVGs found in {}", glyph_dir.display());
let mut out = String::from(
"// @generated by build.rs from assets/glyphs/*.svg — do not edit.\n\
/// Every catalogued icon, sorted by codepoint (`lookup` binary-searches this).\n\
pub static ICONS: &[Icon] = &[\n",
);
for icon in icons.values() {
out.push_str(&format!(
" Icon {{ ch: '\\u{{{cp:04X}}}', name: {name:?}, \
uri: \"bytes://brep-icons/{cp:04X}.svg\", svg: {svg:?}, \
aspect: {aspect:?}, mono: {mono}, artwork_svg: {artwork_svg:?}, artwork_aspect: {artwork_aspect:?} }},\n",
cp = icon.cp,
name = icon.name,
svg = icon.svg,
aspect = icon.aspect,
mono = icon.mono,
artwork_svg = icon.artwork_svg,
artwork_aspect = icon.artwork_aspect,
));
}
out.push_str("];\n");
let dest = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR")).join("icon_catalog.rs");
std::fs::write(&dest, out).unwrap_or_else(|e| panic!("write {}: {e}", dest.display()));
}
struct Icon {
cp: u32,
name: String,
svg: String,
aspect: f32,
mono: bool,
artwork_svg: String,
artwork_aspect: f32,
}
fn parse(path: &Path) -> Option<Icon> {
let text = std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let file = path.display();
let hex = attr(&text, "brep:unicode")?;
if hex.trim().is_empty() {
return None;
}
let cp = u32::from_str_radix(hex.trim(), 16)
.unwrap_or_else(|_| panic!("{file}: brep:unicode = {hex:?} is not hex"));
char::from_u32(cp).unwrap_or_else(|| panic!("{file}: U+{cp:04X} is not a character"));
let vb = attr(&text, "viewBox").unwrap_or_else(|| panic!("{file}: no viewBox"));
let nums: Vec<f32> = vb
.split_whitespace()
.map(|n| n.parse().unwrap_or_else(|_| panic!("{file}: bad viewBox {vb:?}")))
.collect();
let [_, _, w, h] = nums[..] else { panic!("{file}: viewBox needs 4 numbers, got {vb:?}") };
assert!(w > 0.0 && h > 0.0, "{file}: degenerate viewBox {vb:?}");
let mono = fills(&text).all(|f| f.eq_ignore_ascii_case("#111"));
let svg = if mono { text.replace("\"#111\"", "\"#fff\"") } else { text };
let name = attr(&svg, "brep:glyph-name").unwrap_or_else(|| format!("U+{cp:04X}"));
let tree = usvg::Tree::from_str(&svg, &usvg::Options::default())
.unwrap_or_else(|e| panic!("{file}: invalid SVG: {e}"));
let bounds = tree.root().abs_layer_bounding_box();
let serialized = tree.to_string(&usvg::WriteOptions::default());
let body = &serialized[serialized.find('>').expect("SVG root") + 1..];
let artwork_svg = format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"{} {} {} {}\" width=\"{}\" height=\"{}\">{}",
bounds.x(), bounds.y(), bounds.width(), bounds.height(),
bounds.width(), bounds.height(), body,
);
Some(Icon { cp, name, svg, aspect: w / h, mono, artwork_svg,
artwork_aspect: bounds.width() / bounds.height() })
}
fn attr(text: &str, name: &str) -> Option<String> {
let at = text.find(&format!("{name}=\""))? + name.len() + 2;
let rest = &text[at..];
Some(rest[..rest.find('"')?].to_owned())
}
fn fills(text: &str) -> impl Iterator<Item = &str> {
text.match_indices("fill=\"").filter_map(|(i, m)| {
let rest = &text[i + m.len()..];
rest.find('"').map(|end| &rest[..end])
})
}