use crate::enums::{EMU, ONEPT, DEF_FONT_COLOR, is_hex_color};
use crate::types::{Color, PresLayout};
pub fn inch_to_emu(inches: f64) -> i64 {
if inches > 100.0 {
inches as i64
} else {
(EMU as f64 * inches).round() as i64
}
}
pub fn val_to_pts(pt: f64) -> i64 {
(pt * ONEPT as f64).round() as i64
}
pub fn convert_rotation_degrees(d: f64) -> i64 {
let d = if d > 360.0 { d - 360.0 } else { d };
(d * 60_000.0).round() as i64
}
pub fn get_smart_parse_number(value: f64, is_percent: bool, pct_value: f64, layout_dim: i64) -> i64 {
if is_percent {
((pct_value / 100.0) * layout_dim as f64).round() as i64
} else if value < 100.0 {
inch_to_emu(value)
} else {
value as i64
}
}
pub fn rgb_to_hex(r: u8, g: u8, b: u8) -> String {
format!("{:02X}{:02X}{:02X}", r, g, b)
}
pub fn encode_xml_entities(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
pub fn new_uuid() -> String {
uuid::Uuid::new_v4().to_string().to_uppercase()
}
pub fn create_color_element(color_str: &str, inner_elements: Option<&str>) -> String {
let clean = color_str.trim_start_matches('#');
let (is_hex, val) = if is_hex_color(clean) {
(true, clean.to_uppercase())
} else {
let v = if clean.is_empty() { DEF_FONT_COLOR.to_string() } else { clean.to_string() };
(false, v)
};
let tag = if is_hex { "srgbClr" } else { "schemeClr" };
match inner_elements {
Some(inner) if !inner.is_empty() => {
format!("<a:{tag} val=\"{val}\">{inner}</a:{tag}>")
}
_ => format!("<a:{tag} val=\"{val}\"/>"),
}
}
pub fn gen_xml_color_selection_str(color_str: &str, transparency: Option<f64>) -> String {
let clean = color_str.trim_start_matches('#');
if clean.is_empty() {
return String::new();
}
let inner = match transparency {
Some(t) if t > 0.0 => {
let alpha = ((100.0 - t) * 1000.0).round() as i64;
format!("<a:alpha val=\"{alpha}\"/>")
}
_ => String::new(),
};
format!("<a:solidFill>{}</a:solidFill>", create_color_element(clean, Some(&inner)))
}
pub fn gen_xml_color_selection(color: &Color, transparency: Option<f64>) -> String {
match color {
Color::Hex(h) => gen_xml_color_selection_str(h, transparency),
Color::Theme(sc) => {
let inner = match transparency {
Some(t) if t > 0.0 => {
let alpha = ((100.0 - t) * 1000.0).round() as i64;
format!("<a:alpha val=\"{alpha}\"/>")
}
_ => String::new(),
};
format!("<a:solidFill>{}</a:solidFill>",
create_color_element(sc.as_str(), Some(&inner)))
}
Color::ThemedWith(sc, mods) => {
let mut inner = String::new();
if let Some(v) = mods.lum_mod { inner.push_str(&format!("<a:lumMod val=\"{v}\"/>")); }
if let Some(v) = mods.lum_off { inner.push_str(&format!("<a:lumOff val=\"{v}\"/>")); }
if let Some(v) = mods.tint { inner.push_str(&format!("<a:tint val=\"{v}\"/>")); }
if let Some(v) = mods.shade { inner.push_str(&format!("<a:shade val=\"{v}\"/>")); }
if let Some(v) = mods.sat_mod { inner.push_str(&format!("<a:satMod val=\"{v}\"/>")); }
if let Some(t) = transparency {
if t > 0.0 {
let alpha = ((100.0 - t) * 1000.0).round() as i64;
inner.push_str(&format!("<a:alpha val=\"{alpha}\"/>"));
}
}
format!("<a:solidFill>{}</a:solidFill>",
create_color_element(sc.as_str(), Some(&inner)))
}
}
}
pub fn gen_xml_grad_fill(grad: &crate::types::GradientFill) -> String {
use crate::types::GradientType;
let mut s = String::from("<a:gradFill rotWithShape=\"1\"><a:gsLst>");
for stop in &grad.stops {
let pos = (stop.position * 1000.0).round() as i64;
let color_xml = gen_xml_color_selection(&stop.color, stop.transparency);
let inner_xml = color_xml
.strip_prefix("<a:solidFill>").unwrap_or(&color_xml)
.strip_suffix("</a:solidFill>").unwrap_or(&color_xml);
s.push_str(&format!("<a:gs pos=\"{pos}\">{inner_xml}</a:gs>"));
}
s.push_str("</a:gsLst>");
match grad.gradient_type {
GradientType::Linear => {
let ang = (grad.angle * 60_000.0).round() as i64;
s.push_str(&format!("<a:lin ang=\"{ang}\" scaled=\"0\"/>"));
}
GradientType::Radial => {
s.push_str("<a:path path=\"circle\"><a:fillToRect l=\"50000\" t=\"50000\" r=\"50000\" b=\"50000\"/></a:path>");
}
}
s.push_str("</a:gradFill>");
s
}
pub fn create_glow_element(size: f64, color: &str, opacity: f64) -> String {
let rad = val_to_pts(size);
let norm = if opacity > 1.0 { opacity / 100.0 } else { opacity };
let alpha = (norm.clamp(0.0, 1.0) * 100_000.0).round() as i64;
let inner = format!("<a:alpha val=\"{alpha}\"/>");
format!("<a:glow rad=\"{rad}\">{}</a:glow>", create_color_element(color, Some(&inner)))
}
pub fn get_new_rel_id(rels_count: usize, rels_chart_count: usize, rels_media_count: usize) -> u32 {
(rels_count + rels_chart_count + rels_media_count + 1) as u32
}
pub fn coord_to_emu(value: f64, is_percent: bool, pct_value: f64, is_x: bool, layout: &PresLayout) -> i64 {
let layout_dim = if is_x { layout.width } else { layout.height };
get_smart_parse_number(value, is_percent, pct_value, layout_dim)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inch_to_emu() {
assert_eq!(inch_to_emu(1.0), 914_400);
assert_eq!(inch_to_emu(0.5), 457_200);
assert_eq!(inch_to_emu(914_400.0), 914_400);
}
#[test]
fn test_val_to_pts() {
assert_eq!(val_to_pts(1.0), 12_700);
assert_eq!(val_to_pts(12.0), 152_400);
}
#[test]
fn test_convert_rotation() {
assert_eq!(convert_rotation_degrees(90.0), 5_400_000);
assert_eq!(convert_rotation_degrees(270.0), 16_200_000);
}
#[test]
fn test_encode_xml_entities() {
assert_eq!(encode_xml_entities("a & b < c > d"), "a & b < c > d");
assert_eq!(encode_xml_entities("say \"hi\""), "say "hi"");
}
#[test]
fn test_create_color_element_hex() {
assert_eq!(create_color_element("FF0000", None), "<a:srgbClr val=\"FF0000\"/>");
assert_eq!(
create_color_element("FF0000", Some("<a:alpha val=\"75000\"/>")),
"<a:srgbClr val=\"FF0000\"><a:alpha val=\"75000\"/></a:srgbClr>"
);
}
#[test]
fn test_create_color_element_scheme() {
assert_eq!(create_color_element("accent1", None), "<a:schemeClr val=\"accent1\"/>");
}
#[test]
fn test_gen_xml_color_selection() {
let result = gen_xml_color_selection_str("FF0000", None);
assert_eq!(result, "<a:solidFill><a:srgbClr val=\"FF0000\"/></a:solidFill>");
}
}