From 80db9daf0bafdc5408aab09d220efac5df1af7a2 Mon Sep 17 00:00:00 2001 From: rootiest Date: Mon, 3 Aug 2026 22:24:34 -0400 Subject: [PATCH] fix: deduplicate icons in browser by preferring highest resolution version --- src/composite_icon.rs | 79 ++++++++++++++++++++++++++++++++++--------- src/icon_index.rs | 43 ++++++++++++++++++++--- 2 files changed, 102 insertions(+), 20 deletions(-) diff --git a/src/composite_icon.rs b/src/composite_icon.rs index 5731ccd..ea94d0b 100644 --- a/src/composite_icon.rs +++ b/src/composite_icon.rs @@ -25,28 +25,75 @@ impl CompositeIcon { pub fn render(&self) -> String { if let Some(emblem) = &self.emblem { - fn strip_xml(svg: &str) -> &str { - if let Some(idx) = svg.find(" (f64, f64) { + if let Some(re) = Regex::new(r#"(?i)]*viewBox\s*=\s*"([^"]+)""#).ok() { + if let Some(caps) = re.captures(svg) { + let parts: Vec<&str> = caps.get(1).unwrap().as_str().split_whitespace().collect(); + if parts.len() == 4 { + if let (Ok(w), Ok(h)) = (parts[2].parse::(), parts[3].parse::()) { + return (w, h); + } + } + } } + let mut w = 64.0; + let mut h = 64.0; + if let Some(re_w) = Regex::new(r#"(?i)]*width\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#).ok() { + if let Some(caps) = re_w.captures(svg) { + if let Ok(val) = caps.get(1).unwrap().as_str().trim().parse::() { + w = val; + } + } + } + if let Some(re_h) = Regex::new(r#"(?i)]*height\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#).ok() { + if let Some(caps) = re_h.captures(svg) { + if let Ok(val) = caps.get(1).unwrap().as_str().trim().parse::() { + h = val; + } + } + } + (w, h) } - let bg_svg = strip_xml(&self.bg.content); - let emblem_svg = strip_xml(&emblem.content); + fn extract_inner(svg: &str) -> &str { + let start = if let Some(idx) = svg.find("') { + idx + close_idx + 1 + } else { 0 } + } else { 0 }; + let end = if let Some(idx) = svg.rfind("") { idx } else { svg.len() }; + if start < end { &svg[start..end] } else { svg } + } + + let (bg_w, bg_h) = parse_dims(&self.bg.content); + let (em_w, em_h) = parse_dims(&emblem.content); - format!( - r#" - + let mapped_x = self.emblem_x * bg_w / 64.0; + let mapped_y = self.emblem_y * bg_h / 64.0; + let mapped_size = self.emblem_size * bg_w / 64.0; // Assuming uniform coordinate mapping scaling based on width + + let scale_x = mapped_size / em_w; + let scale_y = mapped_size / em_h; // Keep aspect ratio mapping relative to its own viewBox + + let emblem_inner = extract_inner(&emblem.content); + + let emblem_group = format!( + r#" {} - - -{} - + "#, - bg_svg, self.emblem_x, self.emblem_y, self.emblem_size, self.emblem_size, emblem_svg - ) + mapped_x, mapped_y, scale_x, scale_y, emblem_inner + ); + + if let Some(idx) = self.bg.content.rfind("") { + let mut new_svg = self.bg.content[..idx].to_string(); + new_svg.push_str(&emblem_group); + new_svg + } else { + self.bg.content.clone() // fallback + } } else { self.bg.content.clone() } diff --git a/src/icon_index.rs b/src/icon_index.rs index 901efab..a0319c4 100644 --- a/src/icon_index.rs +++ b/src/icon_index.rs @@ -2,13 +2,48 @@ use std::path::{Path, PathBuf}; use walkdir::WalkDir; pub fn list_svg_icons(root: &Path) -> Vec { - WalkDir::new(root) + use std::collections::HashMap; + let mut best_icons: HashMap = HashMap::new(); + + let walker = WalkDir::new(root) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.file_type().is_file()) - .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("svg")) - .map(|e| e.path().to_path_buf()) - .collect() + .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("svg")); + + for entry in walker { + let path = entry.path().to_path_buf(); + let mut components = path.components().rev(); + + let filename = if let Some(c) = components.next() { + c.as_os_str().to_string_lossy().into_owned() + } else { continue; }; + + let size_str = if let Some(c) = components.next() { + c.as_os_str().to_string_lossy().into_owned() + } else { continue; }; + + let category = if let Some(c) = components.next() { + c.as_os_str().to_string_lossy().into_owned() + } else { continue; }; + + let score = if size_str == "scalable" { + 10000 + } else if size_str.ends_with("x2") || size_str.ends_with("@2x") { + size_str.trim_matches(|c: char| !c.is_numeric()).parse::().unwrap_or(0) * 2 + } else { + size_str.parse::().unwrap_or(0) + }; + + let key = format!("{}/{}", category, filename); + + let entry = best_icons.entry(key).or_insert((score, path.clone())); + if score > entry.0 { + *entry = (score, path); + } + } + + best_icons.into_values().map(|(_, p)| p).collect() } pub fn fuzzy_match(query: &str, candidate: &str) -> bool {