// icon-color-tool // Copyright (C) 2026 Rootiest // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use crate::color_scheme::ColorScheme; pub struct CompositeIcon { pub bg: ColorScheme, pub emblem: Option, pub emblem_x: f64, pub emblem_y: f64, pub emblem_size: f64, pub is_modified: bool, } impl CompositeIcon { pub fn new(bg: ColorScheme) -> Self { Self { bg, emblem: None, emblem_x: 32.0, emblem_y: 32.0, emblem_size: 32.0, is_modified: false, } } pub fn set_emblem(&mut self, emblem: ColorScheme) { self.emblem = Some(emblem); } pub fn render(&self) -> String { use regex::Regex; let strip_dimensions = |svg: &str| -> String { if let Some(root_svg_end) = svg.find('>') { let mut root_tag = svg[..=root_svg_end].to_string(); let re_w = Regex::new(r#"(?i)\s+width\s*=\s*"[^"]*""#).unwrap(); root_tag = re_w.replace_all(&root_tag, "").into_owned(); let re_h = Regex::new(r#"(?i)\s+height\s*=\s*"[^"]*""#).unwrap(); root_tag = re_h.replace_all(&root_tag, "").into_owned(); let mut new_svg = root_tag; new_svg.push_str(&svg[root_svg_end + 1..]); new_svg } else { svg.to_string() } }; if let Some(emblem) = &self.emblem { fn parse_dims(svg: &str) -> (f64, f64) { if let Ok(re) = Regex::new(r#"(?i)]*viewBox\s*=\s*"([^"]+)""#) { 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 Ok(re_w) = Regex::new(r#"(?i)]*width\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#) { if let Some(caps) = re_w.captures(svg) { if let Ok(val) = caps.get(1).unwrap().as_str().trim().parse::() { w = val; } } } if let Ok(re_h) = Regex::new(r#"(?i)]*height\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#) { if let Some(caps) = re_h.captures(svg) { if let Ok(val) = caps.get(1).unwrap().as_str().trim().parse::() { h = val; } } } (w, h) } 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); let mapped_size = self.emblem_size * bg_w / 64.0; // Assuming uniform coordinate mapping scaling based on width let mapped_x = (self.emblem_x * bg_w / 64.0) - (mapped_size / 2.0); let mapped_y = (self.emblem_y * bg_h / 64.0) - (mapped_size / 2.0); 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#" {} "#, mapped_x, mapped_y, scale_x, scale_y, emblem_inner ); let base_svg = strip_dimensions(&self.bg.content); if let Some(idx) = base_svg.rfind("") { let mut new_svg = base_svg[..idx].to_string(); new_svg.push_str(&emblem_group); new_svg } else { base_svg } } else { strip_dimensions(&self.bg.content) } } }