use crate::model::{
Alignment, AutoFit, HRelativeFrom, HorizontalPosition, Paragraph, Run, TextFill, TextGlow,
TextOutline, TextShadow, TextWarp, Textbox, VRelativeFrom, VerticalPosition, WrapType,
};
use super::color::{apply_color_transforms, parse_color_transforms};
use super::styles::{StylesInfo, ThemeFonts};
use super::textbox::parse_avlst;
use super::{
DML_NS, W14_NS, dml as find_dml, find_child, parse_hex_color, resolve_theme_color_key,
};
fn find_w14<'a>(
parent: roxmltree::Node<'a, 'a>,
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
find_child(parent, name, W14_NS)
}
pub(super) struct WordArtBodyProps {
pub is_wordart: bool,
pub text_warp: Option<TextWarp>,
pub auto_fit: AutoFit,
}
pub(super) fn parse_wordart_body_pr(body_pr: roxmltree::Node) -> WordArtBodyProps {
let is_wordart = body_pr
.attribute("fromWordArt")
.is_some_and(|v| v == "1" || v == "true");
let text_warp = find_dml(body_pr, "prstTxWarp").and_then(|prst| {
let preset = prst.attribute("prst")?.to_string();
let adjustments = parse_avlst(prst);
Some(TextWarp {
preset,
adjustments,
})
});
let auto_fit = if find_dml(body_pr, "spAutoFit").is_some() {
AutoFit::Shape
} else if let Some(norm) = find_dml(body_pr, "normAutofit") {
let font_scale = norm
.attribute("fontScale")
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v / 100_000.0);
let line_space_reduction = norm
.attribute("lnSpcReduction")
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v / 100_000.0);
AutoFit::Normal {
font_scale,
line_space_reduction,
}
} else {
AutoFit::None
};
WordArtBodyProps {
is_wordart,
text_warp,
auto_fit,
}
}
pub(super) fn parse_text_outline(
rpr: roxmltree::Node,
theme: &ThemeFonts,
) -> Option<TextOutline> {
let outline = find_w14(rpr, "textOutline")?;
let width_pt = outline
.attribute((W14_NS, "w"))
.and_then(|v| v.parse::<f32>().ok())
.map(super::emu_to_pts)
.unwrap_or(0.75);
let color = find_w14_solid_fill_color(outline, theme)?;
Some(TextOutline { width_pt, color })
}
pub(super) fn parse_text_fill(rpr: roxmltree::Node, theme: &ThemeFonts) -> Option<TextFill> {
let text_fill = find_w14(rpr, "textFill")?;
if find_w14(text_fill, "noFill").is_some() {
return Some(TextFill::NoFill);
}
if let Some(color) =
find_w14(text_fill, "solidFill").and_then(|n| resolve_w14_color(n, theme))
{
return Some(TextFill::Solid(color));
}
if let Some(grad) = find_w14(text_fill, "gradFill") {
return parse_w14_gradient(grad, theme);
}
None
}
pub(super) fn parse_text_shadow(rpr: roxmltree::Node, theme: &ThemeFonts) -> Option<TextShadow> {
let shadow = find_w14(rpr, "shadow")?;
let color = find_w14_solid_fill_color(shadow, theme).unwrap_or([128, 128, 128]);
let blur_rad = shadow
.attribute((W14_NS, "blurRad"))
.and_then(|v| v.parse::<f32>().ok())
.map(super::emu_to_pts)
.unwrap_or(0.0);
let dist = shadow
.attribute((W14_NS, "dist"))
.and_then(|v| v.parse::<f32>().ok())
.map(super::emu_to_pts)
.unwrap_or(blur_rad.max(1.5));
let dir = shadow
.attribute((W14_NS, "dir"))
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v / 60_000.0)
.unwrap_or(225.0);
let alpha = find_w14(shadow, "srgbClr")
.and_then(|srgb| find_w14(srgb, "alpha"))
.and_then(|a| a.attribute((W14_NS, "val")))
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v / 100_000.0)
.unwrap_or(0.6);
let dir_rad = dir.to_radians();
let offset_x = dist * dir_rad.cos();
let offset_y = dist * dir_rad.sin();
Some(TextShadow {
color,
offset_x,
offset_y,
alpha,
})
}
pub(super) fn parse_text_glow(rpr: roxmltree::Node, theme: &ThemeFonts) -> Option<TextGlow> {
let glow = find_w14(rpr, "glow")?;
let radius_pt = glow
.attribute((W14_NS, "rad"))
.and_then(|v| v.parse::<f32>().ok())
.map(super::emu_to_pts)
.unwrap_or(0.0);
if radius_pt <= 0.0 {
return None;
}
let color = resolve_w14_color(glow, theme).unwrap_or([255, 255, 0]);
Some(TextGlow { color, radius_pt })
}
pub(super) fn parse_vml_wordart(
shape: roxmltree::Node,
textpath: roxmltree::Node,
styles: &StylesInfo,
_theme: &ThemeFonts,
) -> Option<Textbox> {
let text = textpath.attribute("string")?;
if text.is_empty() {
return None;
}
let style_str = textpath.attribute("style").unwrap_or("");
let (font_name, font_size) = parse_vml_font_style(style_str);
let fill_color = shape
.attribute("fillcolor")
.and_then(|v| parse_hex_color(v.trim_start_matches('#')));
let run = Run {
text: text.to_string(),
font_name: font_name.unwrap_or_else(|| styles.defaults.font_name.clone()),
font_size: font_size.unwrap_or(36.0),
bold: true,
color: fill_color,
..Run::default()
};
let para = Paragraph {
runs: vec![run],
alignment: Alignment::Center,
..Paragraph::default()
};
let shape_style = shape.attribute("style").unwrap_or("");
let (width, height) = parse_vml_dimensions(shape_style);
Some(Textbox {
paragraphs: vec![para],
width_pt: width,
height_pt: height,
h_position: HorizontalPosition::Offset(0.0),
h_relative_from: HRelativeFrom::Column,
v_offset_pt: 0.0,
v_position: VerticalPosition::Offset(0.0),
v_relative_from: VRelativeFrom::Paragraph,
fill: None,
shape_type: Default::default(),
stroke_color: None,
stroke_width: 0.0,
text_anchor: Default::default(),
margin_left: 0.0,
margin_right: 0.0,
margin_top: 0.0,
margin_bottom: 0.0,
wrap_type: WrapType::None,
dist_top: 0.0,
dist_bottom: 0.0,
behind_doc: false,
no_text_wrap: true,
is_wordart: true,
text_warp: None,
auto_fit: AutoFit::Shape,
z_index: 0,
})
}
fn resolve_w14_color(fill_node: roxmltree::Node, theme: &ThemeFonts) -> Option<[u8; 3]> {
if let Some(srgb) = find_w14(fill_node, "srgbClr") {
return srgb.attribute((W14_NS, "val")).and_then(parse_hex_color);
}
if let Some(scheme) = find_w14(fill_node, "schemeClr") {
let val = scheme.attribute((W14_NS, "val"))?;
let theme_key = resolve_theme_color_key(val);
let base = *theme.colors.get(theme_key)?;
let transforms = parse_color_transforms(scheme);
return Some(apply_color_transforms(base, &transforms));
}
if let Some(srgb) = find_child(fill_node, "srgbClr", DML_NS) {
return srgb.attribute("val").and_then(parse_hex_color);
}
if let Some(scheme) = find_child(fill_node, "schemeClr", DML_NS) {
let val = scheme.attribute("val")?;
let theme_key = resolve_theme_color_key(val);
let base = *theme.colors.get(theme_key)?;
let transforms = parse_color_transforms(scheme);
return Some(apply_color_transforms(base, &transforms));
}
None
}
fn find_w14_solid_fill_color(parent: roxmltree::Node, theme: &ThemeFonts) -> Option<[u8; 3]> {
find_w14(parent, "solidFill")
.and_then(|n| resolve_w14_color(n, theme))
.or_else(|| find_dml(parent, "solidFill").and_then(|n| resolve_w14_color(n, theme)))
}
fn parse_w14_gradient(grad: roxmltree::Node, theme: &ThemeFonts) -> Option<TextFill> {
let gs_lst = find_w14(grad, "gsLst")?;
let mut stops = Vec::new();
for gs in gs_lst
.children()
.filter(|n| n.tag_name().name() == "gs" && n.tag_name().namespace() == Some(W14_NS))
{
let pos = gs
.attribute((W14_NS, "pos"))
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v / 100_000.0)
.unwrap_or(0.0);
if let Some(color) = resolve_w14_color(gs, theme) {
stops.push((color, pos));
}
}
if stops.is_empty() {
return None;
}
let angle_deg = find_w14(grad, "lin")
.and_then(|lin| lin.attribute((W14_NS, "ang")))
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v / 60_000.0)
.unwrap_or(0.0);
Some(TextFill::Gradient { stops, angle_deg })
}
fn parse_vml_font_style(style: &str) -> (Option<String>, Option<f32>) {
let mut font_name = None;
let mut font_size = None;
for part in style.split(';') {
if let Some((key, val)) = part.trim().split_once(':') {
let val = val.trim().trim_matches('"').trim_matches('\'');
match key.trim() {
"font-family" => font_name = Some(val.to_string()),
"font-size" => {
font_size = val.trim_end_matches("pt").trim().parse::<f32>().ok();
}
_ => {}
}
}
}
(font_name, font_size)
}
fn parse_vml_dimensions(style: &str) -> (f32, f32) {
let mut width = 0.0_f32;
let mut height = 0.0_f32;
let parse_pt =
|s: &str| -> f32 { s.trim_end_matches("pt").trim().parse::<f32>().unwrap_or(0.0) };
for part in style.split(';') {
if let Some((key, val)) = part.trim().split_once(':') {
match key.trim() {
"width" => width = parse_pt(val.trim()),
"height" => height = parse_pt(val.trim()),
_ => {}
}
}
}
(width, height)
}