Files
icon-color-tool/src/composite_icon.rs
T

138 lines
5.2 KiB
Rust

// 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 <https://www.gnu.org/licenses/>.
use crate::color_scheme::ColorScheme;
pub struct CompositeIcon {
pub bg: ColorScheme,
pub emblem: Option<ColorScheme>,
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)<svg[^>]*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::<f64>(), parts[3].parse::<f64>()) {
return (w, h);
}
}
}
}
let mut w = 64.0;
let mut h = 64.0;
if let Ok(re_w) = Regex::new(r#"(?i)<svg[^>]*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::<f64>() {
w = val;
}
}
}
if let Ok(re_h) = Regex::new(r#"(?i)<svg[^>]*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::<f64>() {
h = val;
}
}
}
(w, h)
}
fn extract_inner(svg: &str) -> &str {
let start = if let Some(idx) = svg.find("<svg") {
if let Some(close_idx) = svg[idx..].find('>') {
idx + close_idx + 1
} else { 0 }
} else { 0 };
let end = if let Some(idx) = svg.rfind("</svg>") { 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#"<g transform="translate({}, {}) scale({}, {})">
{}
</g>
</svg>"#,
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("</svg>") {
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)
}
}
}