use crate::enums::EMU;
use crate::objects::{SlideObject, TextObject, ShapeObject, ImageObject, ConnectorObject, MediaObject, GroupObject};
use crate::objects::text::{TextOptions, TextRun, BulletType, TextFit};
use crate::objects::image::ImageSizingType;
use crate::presentation::Presentation;
use crate::slide::Slide;
use crate::types::{AnimationEffectType, CheckerboardDir, Direction, FillType, PatternFill, ShadowType, ShapeVariant, SplitOrientation, StripDir, TransitionDir};
use crate::utils::{
convert_rotation_degrees, create_glow_element, encode_xml_entities,
gen_xml_color_selection_str, inch_to_emu, val_to_pts,
};
use crate::xml::table_xml::gen_xml_table;
pub fn gen_xml_objects(objects: &[SlideObject], pres: &Presentation) -> String {
let mut s = String::new();
let mut id_counter: usize = 2;
for obj in objects {
render_slide_object(obj, &mut id_counter, pres, &mut s);
}
s
}
fn gen_xml_text_object_fallback(t: &TextObject, obj_id: usize, pres: &Presentation) -> String {
let (x, y, cx, cy) = resolve_position(&t.options.position, pres);
let obj_name = &t.object_name;
let mut s = String::new();
s.push_str("<p:sp>");
s.push_str(&format!(
"<p:nvSpPr>\
<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\"/>\
<p:cNvSpPr txBox=\"1\"><a:spLocks noGrp=\"1\"/></p:cNvSpPr>\
<p:nvPr/>\
</p:nvSpPr>"
));
s.push_str(&format!(
"<p:spPr>\
<a:xfrm><a:off x=\"{x}\" y=\"{y}\"/><a:ext cx=\"{cx}\" cy=\"{cy}\"/></a:xfrm>\
<a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom>\
<a:noFill/>\
</p:spPr>"
));
s.push_str("<p:txBody><a:bodyPr/><a:lstStyle/>");
s.push_str("<a:p>");
for run in &t.text {
if run.equation_omml.is_none() && !run.text.is_empty() {
s.push_str(&format!("<a:r><a:rPr lang=\"en-US\"/><a:t>{}</a:t></a:r>",
encode_xml_entities(&run.text)));
}
}
s.push_str("<a:endParaRPr lang=\"en-US\"/>");
s.push_str("</a:p></p:txBody></p:sp>");
s
}
pub fn slide_object_to_xml(slide: &Slide, pres: &Presentation) -> String {
let mut s = String::new();
s.push_str("<p:cSld>");
if let Some(ref bg) = slide.background {
if let Some(rid) = bg.image_rid {
s.push_str("<p:bg><p:bgPr>");
s.push_str(&format!(
"<a:blipFill dpi=\"0\" rotWithShape=\"1\">\
<a:blip r:embed=\"rId{rid}\"/>\
<a:stretch><a:fillRect/></a:stretch>\
</a:blipFill><a:effectLst/>"
));
s.push_str("</p:bgPr></p:bg>");
} else if let Some(ref color) = bg.color {
s.push_str("<p:bg><p:bgPr>");
s.push_str(&gen_xml_color_selection_str(color, bg.transparency));
s.push_str("<a:effectLst/>");
s.push_str("</p:bgPr></p:bg>");
}
} else {
s.push_str("<p:bg><p:bgRef idx=\"1001\"><a:schemeClr val=\"bg1\"/></p:bgRef></p:bg>");
}
s.push_str("<p:spTree>");
s.push_str("<p:nvGrpSpPr><p:cNvPr id=\"1\" name=\"\"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>");
s.push_str("<p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"0\" cy=\"0\"/><a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"0\" cy=\"0\"/></a:xfrm></p:grpSpPr>");
let mut id_counter: usize = 2; for obj in &slide.objects {
render_slide_object(obj, &mut id_counter, pres, &mut s);
}
if let Some(ref sn) = slide.slide_number {
let sn_id = id_counter;
id_counter += 1;
s.push_str(&gen_xml_slide_number(sn, sn_id, pres));
}
for chart in &slide.charts {
let obj_id = id_counter;
id_counter += 1;
s.push_str(&crate::xml::chart_xml::gen_xml_chart_frame(chart, obj_id, pres));
}
let _ = id_counter;
s.push_str("</p:spTree>");
s.push_str("</p:cSld>");
s
}
fn render_slide_object(obj: &SlideObject, id_counter: &mut usize, pres: &Presentation, s: &mut String) {
let obj_id = *id_counter;
*id_counter += 1;
match obj {
SlideObject::Text(t) => {
let sp_xml = gen_xml_text_object(t, obj_id, pres);
let has_eq = t.text.iter().any(|r| r.equation_omml.is_some());
if has_eq {
s.push_str("<mc:AlternateContent xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\">");
s.push_str("<mc:Choice Requires=\"a14\">");
s.push_str(&sp_xml);
s.push_str("</mc:Choice>");
s.push_str("<mc:Fallback>");
let fallback_id = obj_id + 10000;
s.push_str(&gen_xml_text_object_fallback(t, fallback_id, pres));
s.push_str("</mc:Fallback>");
s.push_str("</mc:AlternateContent>");
} else {
s.push_str(&sp_xml);
}
}
SlideObject::Shape(sh) => s.push_str(&gen_xml_shape_object(sh, obj_id, pres)),
SlideObject::Image(img) => s.push_str(&gen_xml_image_object(img, obj_id, pres)),
SlideObject::Table(tbl) => s.push_str(&gen_xml_table(tbl, obj_id, pres)),
SlideObject::Connector(c) => s.push_str(&gen_xml_connector_object(c, obj_id, pres)),
SlideObject::Media(m) => s.push_str(&gen_xml_media_object(m, obj_id, pres)),
SlideObject::Group(g) => {
s.push_str(&gen_xml_group_object(g, obj_id, id_counter, pres));
}
}
}
fn gen_xml_text_object(t: &TextObject, obj_id: usize, pres: &Presentation) -> String {
let (x, y, cx, cy) = resolve_position(&t.options.position, pres);
let location_attr = build_location_attr(t.options.rotate, t.options.flip_h, t.options.flip_v);
let obj_name = &t.object_name;
let mut s = String::new();
s.push_str("<p:sp>");
s.push_str(&format!(
"<p:nvSpPr>\
<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\"/>\
<p:cNvSpPr txBox=\"1\"><a:spLocks noGrp=\"1\"/></p:cNvSpPr>\
<p:nvPr/>\
</p:nvSpPr>"
));
s.push_str("<p:spPr>");
s.push_str(&format!(
"<a:xfrm{location_attr}><a:off x=\"{x}\" y=\"{y}\"/><a:ext cx=\"{cx}\" cy=\"{cy}\"/></a:xfrm>"
));
s.push_str("<a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom>");
if let Some(ref grad) = t.options.gradient_fill {
s.push_str(&crate::utils::gen_xml_grad_fill(grad));
} else if let Some(ref fill_color) = t.options.fill {
s.push_str(&gen_xml_color_selection_str(fill_color, None));
} else {
s.push_str("<a:noFill/>");
}
if let Some(ref line) = t.options.line {
s.push_str(&gen_xml_shape_line(line));
}
if let Some(ref shadow) = t.options.shadow {
if shadow.shadow_type != ShadowType::None {
s.push_str(&gen_xml_shadow(shadow));
}
}
s.push_str("</p:spPr>");
s.push_str(&gen_xml_text_body_from_runs(&t.text, &t.options, false));
s.push_str("</p:sp>");
s
}
fn gen_xml_shape_object(sh: &ShapeObject, obj_id: usize, pres: &Presentation) -> String {
let (x, y, cx, cy) = resolve_position(&sh.options.position, pres);
let location_attr = build_location_attr(
sh.options.rotate,
sh.options.flip_h,
sh.options.flip_v,
);
let obj_name = &sh.object_name;
let shape_prst = sh.shape_type.as_str();
let mut s = String::new();
s.push_str("<p:sp>");
let alt_attr = sh.options.alt_text.as_deref()
.map(|a| format!(" descr=\"{}\"", encode_xml_entities(a)))
.unwrap_or_default();
let cnv_pr = {
let mut children = String::new();
if let Some(ref hl) = sh.options.hyperlink {
children.push_str(&gen_xml_hlink(hl, "hlinkClick"));
}
if let Some(ref hl) = sh.options.hover {
children.push_str(&gen_xml_hlink(hl, "hlinkHover"));
}
if children.is_empty() {
format!("<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\"{alt_attr}/>")
} else {
format!("<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\"{alt_attr}>{children}</p:cNvPr>")
}
};
s.push_str(&format!(
"<p:nvSpPr>\
{cnv_pr}\
<p:cNvSpPr><a:spLocks noGrp=\"1\"/></p:cNvSpPr>\
<p:nvPr/>\
</p:nvSpPr>"
));
s.push_str("<p:spPr>");
s.push_str(&format!(
"<a:xfrm{location_attr}><a:off x=\"{x}\" y=\"{y}\"/><a:ext cx=\"{cx}\" cy=\"{cy}\"/></a:xfrm>"
));
if let Some(ref pts) = sh.options.custom_geometry {
s.push_str(&gen_xml_custom_geom(pts, cx, cy));
} else {
s.push_str(&format!("<a:prstGeom prst=\"{shape_prst}\">"));
if let Some(r) = sh.options.rect_radius {
let adj = ((r * EMU as f64 * 100_000.0) / cx.max(cy) as f64).round() as i64;
s.push_str(&format!("<a:avLst><a:gd name=\"adj\" fmla=\"val {adj}\"/></a:avLst>"));
} else if let Some([start, swing]) = sh.options.angle_range {
use crate::enums::ShapeType as ST;
let adj1 = (start * 60_000.0).round() as i64;
let adj2 = (swing * 60_000.0).round() as i64;
match sh.shape_type {
ST::BlockArc => {
let inner = ((1.0 - sh.options.arc_thickness.unwrap_or(0.5).clamp(0.0, 1.0)) * 100_000.0).round() as i64;
s.push_str(&format!(
"<a:avLst>\
<a:gd name=\"adj1\" fmla=\"val {adj1}\"/>\
<a:gd name=\"adj2\" fmla=\"val {adj2}\"/>\
<a:gd name=\"adj3\" fmla=\"val {inner}\"/>\
</a:avLst>"
));
}
_ => {
s.push_str(&format!(
"<a:avLst>\
<a:gd name=\"adj1\" fmla=\"val {adj1}\"/>\
<a:gd name=\"adj2\" fmla=\"val {adj2}\"/>\
</a:avLst>"
));
}
}
} else {
s.push_str("<a:avLst/>");
}
s.push_str("</a:prstGeom>");
}
match &sh.options.fill {
Some(fill) => {
match fill.fill_type {
FillType::None => s.push_str("<a:noFill/>"),
FillType::Solid => {
if let Some(ref color) = fill.color {
s.push_str(&crate::utils::gen_xml_color_selection(color, fill.transparency));
} else {
s.push_str("<a:noFill/>");
}
}
FillType::Gradient => {
if let Some(ref grad) = fill.gradient {
s.push_str(&crate::utils::gen_xml_grad_fill(grad));
} else {
s.push_str("<a:noFill/>");
}
}
FillType::Pattern => {
if let Some(ref pf) = fill.pattern {
s.push_str(&gen_xml_pattern_fill(pf));
} else {
s.push_str("<a:noFill/>");
}
}
}
}
None => s.push_str("<a:noFill/>"),
}
if let Some(ref line) = sh.options.line {
s.push_str(&gen_xml_shape_line(line));
}
if let Some(ref shadow) = sh.options.shadow {
if shadow.shadow_type != ShadowType::None {
s.push_str(&gen_xml_shadow(shadow));
}
}
if let Some(ref scene) = sh.options.scene_3d {
s.push_str(&gen_xml_scene3d(scene));
}
if let Some(ref sp3d) = sh.options.shape_3d {
s.push_str(&gen_xml_sp3d(sp3d));
}
s.push_str("</p:spPr>");
if let Some(ref text) = sh.text {
s.push_str(&gen_xml_text_body_from_runs(&text.text, &text.options, false));
} else {
s.push_str("<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang=\"en-US\" dirty=\"0\"/></a:p></p:txBody>");
}
s.push_str("</p:sp>");
s
}
fn gen_xml_image_object(img: &ImageObject, obj_id: usize, pres: &Presentation) -> String {
let (x, y, cx, cy) = resolve_position(&img.options.position, pres);
let mut img_w = cx;
let mut img_h = cy;
let obj_name = &img.object_name;
let alt_text = encode_xml_entities(img.options.alt_text.as_deref().unwrap_or(""));
let rid = img.image_rid;
let location_attr = build_location_attr(img.options.rotate, img.options.flip_h, img.options.flip_v);
let mut s = String::new();
s.push_str("<p:pic>");
s.push_str("<p:nvPicPr>");
{
let mut children = String::new();
if let Some(ref hl) = img.options.hyperlink {
children.push_str(&gen_xml_hlink(hl, "hlinkClick"));
}
if let Some(ref hl) = img.options.hover {
children.push_str(&gen_xml_hlink(hl, "hlinkHover"));
}
if children.is_empty() {
s.push_str(&format!("<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\" descr=\"{alt_text}\"/>"));
} else {
s.push_str(&format!("<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\" descr=\"{alt_text}\">{children}</p:cNvPr>"));
}
}
s.push_str("<p:cNvPicPr><a:picLocks noChangeAspect=\"1\"/></p:cNvPicPr>");
s.push_str("<p:nvPr/>");
s.push_str("</p:nvPicPr>");
s.push_str("<p:blipFill>");
if img.is_svg {
s.push_str(&format!("<a:blip r:embed=\"rId{}\">", rid - 1));
if let Some(t) = img.options.transparency {
s.push_str(&format!("<a:alphaModFix amt=\"{}\"/>", ((100.0 - t) * 1000.0).round() as i64));
}
s.push_str("<a:extLst><a:ext uri=\"{96DAC541-7B7A-43D3-8B79-37D633B846F1}\">");
s.push_str(&format!("<asvg:svgBlip xmlns:asvg=\"http://schemas.microsoft.com/office/drawing/2016/SVG/main\" r:embed=\"rId{rid}\"/>"));
s.push_str("</a:ext></a:extLst>");
s.push_str("</a:blip>");
} else {
s.push_str(&format!("<a:blip r:embed=\"rId{rid}\">"));
if let Some(t) = img.options.transparency {
s.push_str(&format!("<a:alphaModFix amt=\"{}\"/>", ((100.0 - t) * 1000.0).round() as i64));
}
if let Some(ref adj) = img.options.color_adjust {
if adj.brightness.is_some() || adj.contrast.is_some() {
let bright = adj.brightness.map(|b| (b * 1000.0).round() as i64).unwrap_or(0);
let contrast = adj.contrast.map(|c| (c * 1000.0).round() as i64).unwrap_or(0);
s.push_str(&format!("<a:lum bright=\"{bright}\" contrast=\"{contrast}\"/>"));
}
if adj.grayscale {
s.push_str("<a:grayscl/>");
}
}
s.push_str("</a:blip>");
}
if let Some(ref sizing) = img.options.sizing {
let box_w = sizing.w.map(inch_to_emu).unwrap_or(cx);
let box_h = sizing.h.map(inch_to_emu).unwrap_or(cy);
let box_x = sizing.x.map(inch_to_emu).unwrap_or(0);
let box_y = sizing.y.map(inch_to_emu).unwrap_or(0);
let img_size = (cx, cy);
let box_dim = (box_w, box_h, box_x, box_y);
s.push_str(&image_sizing_xml(&sizing.sizing_type, img_size, box_dim));
img_w = box_w;
img_h = box_h;
} else if let Some(ref crop) = img.options.crop {
let l = (crop[0] * 100_000.0).round() as i64;
let t = (crop[1] * 100_000.0).round() as i64;
let r = (crop[2] * 100_000.0).round() as i64;
let b = (crop[3] * 100_000.0).round() as i64;
s.push_str(&format!("<a:srcRect l=\"{l}\" t=\"{t}\" r=\"{r}\" b=\"{b}\"/><a:stretch><a:fillRect/></a:stretch>"));
} else {
s.push_str("<a:stretch><a:fillRect/></a:stretch>");
}
s.push_str("</p:blipFill>");
s.push_str("<p:spPr>");
s.push_str(&format!(
"<a:xfrm{location_attr}><a:off x=\"{x}\" y=\"{y}\"/><a:ext cx=\"{img_w}\" cy=\"{img_h}\"/></a:xfrm>"
));
let geom = if img.options.rounding { "ellipse" } else { "rect" };
s.push_str(&format!("<a:prstGeom prst=\"{geom}\"><a:avLst/></a:prstGeom>"));
if let Some(ref shadow) = img.options.shadow {
if shadow.shadow_type != ShadowType::None {
s.push_str(&gen_xml_shadow(shadow));
}
}
s.push_str("</p:spPr>");
s.push_str("</p:pic>");
s
}
fn image_sizing_xml(
sizing_type: &ImageSizingType,
img_size: (i64, i64),
box_dim: (i64, i64, i64, i64),
) -> String {
let (img_w, img_h) = (img_size.0 as f64, img_size.1 as f64);
let (box_w, box_h, box_x, box_y) = (box_dim.0 as f64, box_dim.1 as f64, box_dim.2 as f64, box_dim.3 as f64);
match sizing_type {
ImageSizingType::Cover => {
let img_ratio = img_h / img_w;
let box_ratio = box_h / box_w;
let is_box_based = box_ratio > img_ratio;
let width = if is_box_based { box_h / img_ratio } else { box_w };
let height = if is_box_based { box_h } else { box_w * img_ratio };
let hz = ((0.5 * (1.0 - box_w / width)) * 1e5).round() as i64;
let vz = ((0.5 * (1.0 - box_h / height)) * 1e5).round() as i64;
format!("<a:srcRect l=\"{hz}\" r=\"{hz}\" t=\"{vz}\" b=\"{vz}\"/><a:stretch/>")
}
ImageSizingType::Contain => {
let img_ratio = img_h / img_w;
let box_ratio = box_h / box_w;
let width_based = box_ratio > img_ratio;
let width = if width_based { box_w } else { box_h / img_ratio };
let height = if width_based { box_w * img_ratio } else { box_h };
let hz = ((0.5 * (1.0 - box_w / width)) * 1e5).round() as i64;
let vz = ((0.5 * (1.0 - box_h / height)) * 1e5).round() as i64;
format!("<a:srcRect l=\"{hz}\" r=\"{hz}\" t=\"{vz}\" b=\"{vz}\"/><a:stretch/>")
}
ImageSizingType::Crop => {
let l = box_x;
let r = img_w - (box_x + box_w);
let t = box_y;
let b = img_h - (box_y + box_h);
let lp = ((l / img_w) * 1e5).round() as i64;
let rp = ((r / img_w) * 1e5).round() as i64;
let tp = ((t / img_h) * 1e5).round() as i64;
let bp = ((b / img_h) * 1e5).round() as i64;
format!("<a:srcRect l=\"{lp}\" r=\"{rp}\" t=\"{tp}\" b=\"{bp}\"/><a:stretch/>")
}
}
}
fn gen_xml_group_object(g: &GroupObject, obj_id: usize, id_counter: &mut usize, pres: &Presentation) -> String {
let (x, y, cx, cy) = resolve_position(&g.position, pres);
let obj_name = &g.object_name;
let (ch_off_x, ch_off_y) = g.child_offset;
let (ch_ext_cx, ch_ext_cy) = g.child_extent;
let mut s = String::new();
s.push_str("<p:grpSp>");
s.push_str(&format!(
"<p:nvGrpSpPr>\
<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\"/>\
<p:cNvGrpSpPr/>\
<p:nvPr/>\
</p:nvGrpSpPr>"
));
s.push_str(&format!(
"<p:grpSpPr><a:xfrm>\
<a:off x=\"{x}\" y=\"{y}\"/>\
<a:ext cx=\"{cx}\" cy=\"{cy}\"/>\
<a:chOff x=\"{ch_off_x}\" y=\"{ch_off_y}\"/>\
<a:chExt cx=\"{ch_ext_cx}\" cy=\"{ch_ext_cy}\"/>\
</a:xfrm></p:grpSpPr>"
));
for child in &g.children {
render_slide_object(child, id_counter, pres, &mut s);
}
s.push_str("</p:grpSp>");
s
}
fn gen_xml_media_object(m: &MediaObject, obj_id: usize, pres: &Presentation) -> String {
let (x, y, cx, cy) = resolve_position(&m.options.position, pres);
let obj_name = &m.object_name;
let alt_text = m.options.alt_text.as_deref().unwrap_or("");
let media_rid = m.media_rid;
let mut s = String::new();
s.push_str("<p:pic>");
s.push_str("<p:nvPicPr>");
s.push_str(&format!(
"<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\" descr=\"{alt_text}\"/>"
));
s.push_str("<p:cNvPicPr><a:picLocks noChangeAspect=\"1\"/></p:cNvPicPr>");
s.push_str("<p:nvPr>");
match m.media_type {
crate::objects::MediaType::Video => {
s.push_str(&format!("<a:videoFile r:link=\"rId{media_rid}\"/>"));
}
crate::objects::MediaType::Audio => {
s.push_str(&format!("<a:audioFile r:link=\"rId{media_rid}\"/>"));
}
}
s.push_str("</p:nvPr>");
s.push_str("</p:nvPicPr>");
s.push_str("<p:blipFill>");
if let Some(poster_rid) = m.poster_rid {
s.push_str(&format!("<a:blip r:embed=\"rId{poster_rid}\"/>"));
} else {
s.push_str("<a:blip/>");
}
s.push_str("<a:stretch><a:fillRect/></a:stretch>");
s.push_str("</p:blipFill>");
s.push_str("<p:spPr>");
s.push_str(&format!(
"<a:xfrm><a:off x=\"{x}\" y=\"{y}\"/><a:ext cx=\"{cx}\" cy=\"{cy}\"/></a:xfrm>"
));
s.push_str("<a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom>");
s.push_str("</p:spPr>");
s.push_str("</p:pic>");
s
}
fn gen_xml_hlink(hl: &crate::types::HyperlinkProps, element: &str) -> String {
let tooltip = hl.tooltip.as_deref().unwrap_or("");
let tooltip_attr = if tooltip.is_empty() { String::new() } else { format!(" tooltip=\"{}\"", encode_xml_entities(tooltip)) };
if let Some(ref nav) = hl.action {
format!("<a:{element} r:id=\"\" action=\"{}\"{tooltip_attr}/>", nav.as_ppaction())
} else if hl.r_id > 0 {
if hl.slide.is_some() {
format!("<a:{element} r:id=\"rId{}\" action=\"ppaction://hlinksldjump\"{tooltip_attr}/>", hl.r_id)
} else {
format!("<a:{element} r:id=\"rId{}\"{tooltip_attr}/>", hl.r_id)
}
} else {
String::new()
}
}
pub fn gen_xml_text_body_from_runs(
runs: &[TextRun],
opts: &TextOptions,
is_table_cell: bool,
) -> String {
let tag = if is_table_cell { "a:txBody" } else { "p:txBody" };
let mut s = String::new();
s.push_str(&format!("<{tag}>"));
s.push_str(&gen_xml_body_properties(opts, is_table_cell));
s.push_str("<a:lstStyle/>");
let paragraphs = group_runs_into_paragraphs(runs);
for para in ¶graphs {
s.push_str("<a:p>");
s.push_str(&gen_xml_para_props(opts, false));
for run in para {
if run.text.is_empty() && run.field.is_none() && run.equation_omml.is_none() {
s.push_str("<a:endParaRPr lang=\"en-US\" dirty=\"0\"/>");
} else {
if run.soft_break_before {
s.push_str("<a:br><a:rPr lang=\"en-US\" dirty=\"0\"/></a:br>");
}
s.push_str(&gen_xml_text_run(run, opts));
}
}
s.push_str("<a:endParaRPr lang=\"en-US\" dirty=\"0\"/>");
s.push_str("</a:p>");
}
if paragraphs.is_empty() {
s.push_str("<a:p><a:endParaRPr lang=\"en-US\" dirty=\"0\"/></a:p>");
}
s.push_str(&format!("</{tag}>"));
s
}
fn group_runs_into_paragraphs(runs: &[TextRun]) -> Vec<Vec<&TextRun>> {
let mut paragraphs: Vec<Vec<&TextRun>> = Vec::new();
let mut current: Vec<&TextRun> = Vec::new();
for run in runs {
current.push(run);
if run.break_line {
paragraphs.push(current);
current = Vec::new();
}
}
if !current.is_empty() {
paragraphs.push(current);
}
if paragraphs.is_empty() {
paragraphs.push(Vec::new());
}
paragraphs
}
fn gen_xml_body_properties(opts: &TextOptions, is_table_cell: bool) -> String {
if is_table_cell {
return "<a:bodyPr/>".to_string();
}
let mut s = String::from("<a:bodyPr");
let wrap = opts.wrap.unwrap_or(true);
s.push_str(&format!(" wrap=\"{}\"", if wrap { "square" } else { "none" }));
if let Some(ref vert) = opts.vert {
s.push_str(&format!(" vert=\"{vert}\""));
}
if let Some(ref bp) = opts.body_prop {
if let Some(v) = bp.l_ins { s.push_str(&format!(" lIns=\"{v}\"")); }
if let Some(v) = bp.t_ins { s.push_str(&format!(" tIns=\"{v}\"")); }
if let Some(v) = bp.r_ins { s.push_str(&format!(" rIns=\"{v}\"")); }
if let Some(v) = bp.b_ins { s.push_str(&format!(" bIns=\"{v}\"")); }
if let Some(ref anchor) = bp.anchor { s.push_str(&format!(" anchor=\"{anchor}\"")); }
if let Some(ref vert2) = bp.vert { s.push_str(&format!(" vert=\"{vert2}\"")); }
} else if let Some(ref margin) = opts.margin {
s.push_str(&format!(" lIns=\"{}\"", val_to_pts(margin.left())));
s.push_str(&format!(" tIns=\"{}\"", val_to_pts(margin.top())));
s.push_str(&format!(" rIns=\"{}\"", val_to_pts(margin.right())));
s.push_str(&format!(" bIns=\"{}\"", val_to_pts(margin.bottom())));
if let Some(ref valign) = opts.valign {
s.push_str(&format!(" anchor=\"{}\"", valign.as_ooxml()));
}
}
s.push_str(" rtlCol=\"0\"");
if let Some(n) = opts.num_columns {
if n >= 2 {
s.push_str(&format!(" numCol=\"{n}\""));
if let Some(gap) = opts.column_spacing {
s.push_str(&format!(" spcCol=\"{}\"", inch_to_emu(gap)));
}
}
}
let mut children = String::new();
match opts.fit {
Some(TextFit::None) => children.push_str("<a:noAutofit/>"),
Some(TextFit::Shrink) => children.push_str("<a:normAutofit/>"),
Some(TextFit::Resize) => children.push_str("<a:spAutoFit/>"),
_ => {}
}
if let Some(ref bp) = opts.body_prop {
if bp.auto_fit { children.push_str("<a:spAutoFit/>"); }
}
if children.is_empty() {
s.push_str("/>");
} else {
s.push('>');
s.push_str(&children);
s.push_str("</a:bodyPr>");
}
s
}
fn gen_xml_para_props(opts: &TextOptions, _is_default: bool) -> String {
let mut s = String::new();
let has_align = opts.align.is_some();
let has_line = opts.line_spacing.is_some() || opts.line_spacing_multiple.is_some();
let _has_bullet = opts.bullet.is_none(); let has_indent = opts.indent_level.map_or(false, |l| l > 0);
let rtl = opts.rtl_mode;
let has_tab_stops = opts.tab_stops.as_ref().map_or(false, |t| !t.is_empty());
if !has_align && !has_line && !has_indent && !rtl && !has_tab_stops {
s.push_str("<a:pPr indent=\"0\" marL=\"0\"><a:buNone/></a:pPr>");
return s;
}
s.push_str("<a:pPr");
if rtl { s.push_str(" rtl=\"1\""); }
if let Some(ref align) = opts.align {
s.push_str(&format!(" algn=\"{}\"", align.as_ooxml()));
}
if let Some(level) = opts.indent_level {
if level > 0 { s.push_str(&format!(" lvl=\"{level}\"")); }
}
s.push_str(" indent=\"0\" marL=\"0\">");
if let Some(ls) = opts.line_spacing {
s.push_str(&format!("<a:lnSpc><a:spcPts val=\"{}\"/></a:lnSpc>", (ls * 100.0).round() as i64));
} else if let Some(lm) = opts.line_spacing_multiple {
s.push_str(&format!("<a:lnSpc><a:spcPct val=\"{}\"/></a:lnSpc>", (lm * 100_000.0).round() as i64));
}
if let Some(before) = opts.para_space_before {
s.push_str(&format!("<a:spcBef><a:spcPts val=\"{}\"/></a:spcBef>", (before * 100.0).round() as i64));
}
if let Some(after) = opts.para_space_after {
s.push_str(&format!("<a:spcAft><a:spcPts val=\"{}\"/></a:spcAft>", (after * 100.0).round() as i64));
}
match &opts.bullet {
Some(bullet) => {
match bullet.bullet_type {
BulletType::Default => {
s.push_str("<a:buSzPct val=\"100000\"/><a:buChar char=\"•\"/>");
}
BulletType::Numbered => {
let style = bullet.style.as_deref().unwrap_or("arabicPeriod");
let start = bullet.number_start_at.unwrap_or(1);
s.push_str(&format!("<a:buSzPct val=\"100000\"/><a:buFont typeface=\"+mj-lt\"/><a:buAutoNum type=\"{style}\" startAt=\"{start}\"/>"));
}
BulletType::Character => {
if let Some(ref code) = bullet.character_code {
s.push_str(&format!("<a:buSzPct val=\"100000\"/><a:buChar char=\"&#{code};\"/>"));
}
}
}
}
None => {
s.push_str("<a:buNone/>");
}
}
if let Some(ref stops) = opts.tab_stops {
if !stops.is_empty() {
s.push_str("<a:tabLst>");
for stop in stops {
let pos = inch_to_emu(stop.pos_inches);
let algn = &stop.align;
s.push_str(&format!("<a:tab pos=\"{pos}\" algn=\"{algn}\"/>"));
}
s.push_str("</a:tabLst>");
}
}
s.push_str("</a:pPr>");
s
}
pub fn gen_xml_text_run(run: &TextRun, para_opts: &TextOptions) -> String {
let run_opts = &run.options;
let mut s = String::new();
if let Some(ref omml) = run.equation_omml {
s.push_str(omml);
return s;
}
if let Some(ref fld) = run.field {
let fld_type = fld.as_ooxml();
let guid = format!("{{B0E4D6F0-0000-0000-0000-{:0>12X}}}", {
let mut hash: u64 = 5381;
for b in fld_type.bytes() { hash = hash.wrapping_mul(33).wrapping_add(b as u64); }
hash & 0xFFFF_FFFF_FFFF
});
s.push_str(&format!("<a:fld id=\"{guid}\" type=\"{fld_type}\">"));
s.push_str("<a:rPr lang=\"en-US\" smtClean=\"0\"/>");
let placeholder = if run.text.is_empty() { "\u{2039}#\u{203A}" } else { &run.text };
s.push_str(&format!("<a:t>{}</a:t>", encode_xml_entities(placeholder)));
s.push_str("</a:fld>");
return s;
}
s.push_str("<a:r>");
let lang = run_opts.lang.as_deref().unwrap_or("en-US");
let mut rpr_attrs = format!("<a:rPr lang=\"{lang}\"");
let font_size = run_opts.font_size.or(para_opts.font_size);
if let Some(fs) = font_size {
rpr_attrs.push_str(&format!(" sz=\"{}\"", (fs * 100.0).round() as i64));
}
let bold = run_opts.bold.or(para_opts.bold);
if let Some(b) = bold { rpr_attrs.push_str(&format!(" b=\"{}\"", if b { 1 } else { 0 })); }
let italic = run_opts.italic.or(para_opts.italic);
if let Some(i) = italic { rpr_attrs.push_str(&format!(" i=\"{}\"", if i { 1 } else { 0 })); }
if let Some(ref u) = run_opts.underline { rpr_attrs.push_str(&format!(" u=\"{u}\"")); }
if let Some(ref st) = run_opts.strike {
let val = if st == "dbl" { "dblStrike" } else { "sngStrike" };
rpr_attrs.push_str(&format!(" strike=\"{val}\""));
}
if run_opts.superscript { rpr_attrs.push_str(" baseline=\"30000\""); }
else if run_opts.subscript { rpr_attrs.push_str(" baseline=\"-40000\""); }
if let Some(cs) = run_opts.char_spacing {
rpr_attrs.push_str(&format!(" spc=\"{}\" kern=\"0\"", (cs * 100.0).round() as i64));
}
rpr_attrs.push_str(" dirty=\"0\"");
let mut rpr_children = String::new();
if let Some(ref outline) = run_opts.outline {
let w = val_to_pts(outline.size);
let c = outline.color.trim_start_matches('#').to_uppercase();
rpr_children.push_str(&format!("<a:ln w=\"{w}\"><a:solidFill><a:srgbClr val=\"{c}\"/></a:solidFill></a:ln>"));
}
if let Some(ref uc) = run_opts.underline_color {
let c = uc.trim_start_matches('#').to_uppercase();
rpr_children.push_str(&format!("<a:uFill><a:solidFill><a:srgbClr val=\"{c}\"/></a:solidFill></a:uFill>"));
}
let color = run_opts.color.as_deref().or(para_opts.color.as_deref());
if let Some(c) = color {
rpr_children.push_str(&gen_xml_color_selection_str(c, run_opts.transparency));
} else if let Some(t) = run_opts.transparency {
let alpha = ((100.0 - t) * 1000.0).round() as i64;
rpr_children.push_str(&format!(
"<a:solidFill><a:srgbClr val=\"000000\"><a:alpha val=\"{alpha}\"/></a:srgbClr></a:solidFill>"
));
}
if let Some(ref glow) = run_opts.glow {
rpr_children.push_str(&format!("<a:effectLst>{}</a:effectLst>",
create_glow_element(glow.size, &glow.color, glow.opacity)));
}
if let Some(ref hl_color) = run_opts.highlight {
let c = hl_color.trim_start_matches('#').to_uppercase();
rpr_children.push_str(&format!("<a:highlight><a:srgbClr val=\"{c}\"/></a:highlight>"));
}
let font_face = run_opts.font_face.as_deref().or(para_opts.font_face.as_deref());
if let Some(ff) = font_face {
rpr_children.push_str(&format!(
"<a:latin typeface=\"{ff}\" pitchFamily=\"34\" charset=\"0\"/>\
<a:ea typeface=\"{ff}\" pitchFamily=\"34\" charset=\"-122\"/>\
<a:cs typeface=\"{ff}\" pitchFamily=\"34\" charset=\"-120\"/>"
));
}
if let Some(ref hl) = run_opts.hyperlink {
let tooltip = hl.tooltip.as_deref().unwrap_or("");
let tooltip_attr = if tooltip.is_empty() { String::new() } else { format!(" tooltip=\"{}\"", encode_xml_entities(tooltip)) };
if let Some(ref nav) = hl.action {
rpr_children.push_str(&format!("<a:hlinkClick r:id=\"\" action=\"{}\"{tooltip_attr}/>", nav.as_ppaction()));
} else if hl.r_id > 0 {
if hl.slide.is_some() {
rpr_children.push_str(&format!(
"<a:hlinkClick r:id=\"rId{}\" action=\"ppaction://hlinksldjump\"{tooltip_attr}/>",
hl.r_id
));
} else if hl.url.is_some() {
rpr_children.push_str(&format!(
"<a:hlinkClick r:id=\"rId{}\"{tooltip_attr}/>",
hl.r_id
));
}
}
}
if rpr_children.is_empty() {
s.push_str(&rpr_attrs);
s.push_str("/>");
} else {
s.push_str(&rpr_attrs);
s.push('>');
s.push_str(&rpr_children);
s.push_str("</a:rPr>");
}
s.push_str(&format!("<a:t>{}</a:t>", encode_xml_entities(&run.text)));
s.push_str("</a:r>");
s
}
fn gen_xml_slide_number(sn: &crate::types::SlideNumberProps, obj_id: usize, pres: &Presentation) -> String {
let layout = &pres.layout;
let x = sn.x.as_ref().map(|c| c.to_emu(layout.width)).unwrap_or_else(|| inch_to_emu(0.1));
let y = sn.y.as_ref().map(|c| c.to_emu(layout.height)).unwrap_or_else(|| inch_to_emu(7.0));
let cx = sn.w.as_ref().map(|c| c.to_emu(layout.width)).unwrap_or_else(|| inch_to_emu(1.0));
let cy = sn.h.as_ref().map(|c| c.to_emu(layout.height)).unwrap_or_else(|| inch_to_emu(0.4));
let mut rpr_attrs = String::from("<a:rPr lang=\"en-US\"");
if let Some(fs) = sn.font_size {
rpr_attrs.push_str(&format!(" sz=\"{}\"", (fs * 100.0).round() as i64));
}
if sn.bold { rpr_attrs.push_str(" b=\"1\""); }
rpr_attrs.push_str(" smtClean=\"0\" dirty=\"0\"");
let mut rpr_children = String::new();
if let Some(ref c) = sn.color {
rpr_children.push_str(&gen_xml_color_selection_str(c, None));
}
if let Some(ref ff) = sn.font_face {
rpr_children.push_str(&format!(
"<a:latin typeface=\"{ff}\" pitchFamily=\"34\" charset=\"0\"/>"
));
}
let rpr = if rpr_children.is_empty() {
format!("{rpr_attrs}/>")
} else {
format!("{rpr_attrs}>{rpr_children}</a:rPr>")
};
let align_attr = sn.align.as_deref().map(|a| format!(" algn=\"{a}\"")).unwrap_or_default();
format!(
"<p:sp>\
<p:nvSpPr>\
<p:cNvPr id=\"{obj_id}\" name=\"Slide Number Placeholder {obj_id}\"/>\
<p:cNvSpPr><a:spLocks noGrp=\"1\"/></p:cNvSpPr>\
<p:nvPr><p:ph type=\"sldNum\" sz=\"quarter\" idx=\"12\"/></p:nvPr>\
</p:nvSpPr>\
<p:spPr>\
<a:xfrm><a:off x=\"{x}\" y=\"{y}\"/><a:ext cx=\"{cx}\" cy=\"{cy}\"/></a:xfrm>\
<a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom>\
</p:spPr>\
<p:txBody>\
<a:bodyPr/>\
<a:lstStyle/>\
<a:p>\
<a:pPr{align_attr}/>\
<a:fld id=\"{{F7021451-1387-4CA6-816F-3879F97B5CBC}}\" type=\"slidenum\">\
{rpr}\
<a:t>‹#›</a:t>\
</a:fld>\
</a:p>\
</p:txBody>\
</p:sp>"
)
}
fn gen_xml_custom_geom(pts: &[crate::types::CustomGeomPoint], _cx: i64, _cy: i64) -> String {
use crate::types::CustomGeomPoint as CGP;
const W: i64 = 100_000;
const H: i64 = 100_000;
let px = |x: f64| (x * W as f64).round() as i64;
let py = |y: f64| (y * H as f64).round() as i64;
let mut ops = String::new();
for pt in pts {
match pt {
CGP::MoveTo(x, y) => {
ops.push_str(&format!("<a:moveTo><a:pt x=\"{}\" y=\"{}\"/></a:moveTo>", px(*x), py(*y)));
}
CGP::LineTo(x, y) => {
ops.push_str(&format!("<a:lnTo><a:pt x=\"{}\" y=\"{}\"/></a:lnTo>", px(*x), py(*y)));
}
CGP::ArcTo { w_r, h_r, start_angle, swing_angle } => {
let wr = px(*w_r);
let hr = py(*h_r);
let st = (*start_angle * 60_000.0).round() as i64;
let sw = (*swing_angle * 60_000.0).round() as i64;
ops.push_str(&format!("<a:arcTo wR=\"{wr}\" hR=\"{hr}\" stAng=\"{st}\" swAng=\"{sw}\"/>"));
}
CGP::CubicBezTo(cp1x, cp1y, cp2x, cp2y, ex, ey) => {
ops.push_str(&format!(
"<a:cubicBezTo>\
<a:pt x=\"{}\" y=\"{}\"/>\
<a:pt x=\"{}\" y=\"{}\"/>\
<a:pt x=\"{}\" y=\"{}\"/>\
</a:cubicBezTo>",
px(*cp1x), py(*cp1y), px(*cp2x), py(*cp2y), px(*ex), py(*ey)
));
}
CGP::QuadBezTo(cpx, cpy, ex, ey) => {
ops.push_str(&format!(
"<a:quadBezTo>\
<a:pt x=\"{}\" y=\"{}\"/>\
<a:pt x=\"{}\" y=\"{}\"/>\
</a:quadBezTo>",
px(*cpx), py(*cpy), px(*ex), py(*ey)
));
}
CGP::Close => {
ops.push_str("<a:close/>");
}
}
}
format!(
"<a:custGeom>\
<a:avLst/>\
<a:gdLst/>\
<a:ahLst/>\
<a:cxnLst/>\
<a:rect l=\"0\" t=\"0\" r=\"{W}\" b=\"{H}\"/>\
<a:pathLst>\
<a:path w=\"{W}\" h=\"{H}\">{ops}</a:path>\
</a:pathLst>\
</a:custGeom>"
)
}
fn gen_xml_shape_line(line: &crate::types::ShapeLineProps) -> String {
let width_attr = line.width
.map(|w| format!(" w=\"{}\"", val_to_pts(w)))
.unwrap_or_default();
let cap_attr = line.cap
.map(|c| format!(" cap=\"{}\"", c.as_str()))
.unwrap_or_default();
let mut s = format!("<a:ln{width_attr}{cap_attr}>");
if let Some(ref color) = line.color {
s.push_str(&gen_xml_color_selection_str(color, line.transparency));
}
if let Some(ref dash) = line.dash_type {
s.push_str(&format!("<a:prstDash val=\"{dash}\"/>"));
}
match line.join {
Some(crate::types::LineJoin::Round) => s.push_str("<a:round/>"),
Some(crate::types::LineJoin::Bevel) => s.push_str("<a:bevel/>"),
Some(crate::types::LineJoin::Miter) => s.push_str("<a:miter/>"),
None => {}
}
if let Some(ref arrow) = line.begin_arrow_type {
s.push_str(&format!("<a:headEnd type=\"{arrow}\"/>"));
}
if let Some(ref arrow) = line.end_arrow_type {
s.push_str(&format!("<a:tailEnd type=\"{arrow}\"/>"));
}
s.push_str("</a:ln>");
s
}
fn gen_xml_connector_object(c: &ConnectorObject, obj_id: usize, pres: &Presentation) -> String {
let layout = &pres.layout;
let x1 = c.options.x1.as_ref().map(|v| v.to_emu(layout.width)).unwrap_or(0);
let y1 = c.options.y1.as_ref().map(|v| v.to_emu(layout.height)).unwrap_or(0);
let x2 = c.options.x2.as_ref().map(|v| v.to_emu(layout.width)).unwrap_or(0);
let y2 = c.options.y2.as_ref().map(|v| v.to_emu(layout.height)).unwrap_or(0);
let bx = x1.min(x2);
let by = y1.min(y2);
let cx = (x2 - x1).unsigned_abs() as i64;
let cy = (y2 - y1).unsigned_abs() as i64;
let flip_h = if x2 < x1 { " flipH=\"1\"" } else { "" };
let flip_v = if y2 < y1 { " flipV=\"1\"" } else { "" };
let prst = c.connector_type.as_prst();
let obj_name = &c.object_name;
let st_cxn = if let Some(ref ep) = c.options.start_conn {
format!("<a:stCxn id=\"{}\" idx=\"{}\"/>", ep.shape_id, ep.site_idx)
} else {
String::new()
};
let end_cxn = if let Some(ref ep) = c.options.end_conn {
format!("<a:endCxn id=\"{}\" idx=\"{}\"/>", ep.shape_id, ep.site_idx)
} else {
String::new()
};
let mut s = String::new();
s.push_str(&format!(
"<p:cxnSp>\
<p:nvCxnSpPr>\
<p:cNvPr id=\"{obj_id}\" name=\"{obj_name}\"/>\
<p:cNvCxnSpPr>{st_cxn}{end_cxn}</p:cNvCxnSpPr>\
<p:nvPr/>\
</p:nvCxnSpPr>\
<p:spPr>\
<a:xfrm{flip_h}{flip_v}>\
<a:off x=\"{bx}\" y=\"{by}\"/>\
<a:ext cx=\"{cx}\" cy=\"{cy}\"/>\
</a:xfrm>\
<a:prstGeom prst=\"{prst}\"><a:avLst/></a:prstGeom>\
<a:noFill/>"
));
if let Some(ref line) = c.options.line {
let width_attr = line.width
.map(|w| format!(" w=\"{}\"", val_to_pts(w)))
.unwrap_or_default();
s.push_str(&format!("<a:ln{width_attr}>"));
if let Some(ref color) = line.color {
s.push_str(&gen_xml_color_selection_str(color, line.transparency));
}
if let Some(ref dash) = line.dash_type {
s.push_str(&format!("<a:prstDash val=\"{dash}\"/>"));
}
if let Some(ref arrow) = line.begin_arrow_type {
s.push_str(&format!("<a:headEnd type=\"{arrow}\"/>"));
}
if let Some(ref arrow) = line.end_arrow_type {
s.push_str(&format!("<a:tailEnd type=\"{arrow}\"/>"));
}
s.push_str("</a:ln>");
}
s.push_str("</p:spPr>");
s.push_str("</p:cxnSp>");
s
}
pub fn gen_xml_transition(slide: &Slide) -> String {
use crate::types::{TransitionType};
let props = match &slide.transition {
Some(p) => p,
None => return String::new(),
};
if props.transition_type == TransitionType::None {
return String::new();
}
let mut attrs = String::new();
if let Some(ms) = props.duration_ms {
attrs.push_str(&format!(" dur=\"{ms}\""));
} else if let Some(ref spd) = props.speed {
attrs.push_str(&format!(" spd=\"{}\"", spd.as_str()));
}
if let Some(ms) = props.advance_after_ms {
attrs.push_str(&format!(" advTm=\"{ms}\""));
}
if !props.advance_on_click {
attrs.push_str(" advClick=\"0\"");
}
let dir_attr = props.direction.as_ref()
.filter(|d| !matches!(d, TransitionDir::Left))
.map(|d| format!(" dir=\"{}\"", d.as_str()))
.unwrap_or_default();
let is_p14 = matches!(&props.transition_type,
TransitionType::Flash | TransitionType::Vortex | TransitionType::Ripple |
TransitionType::Glitter | TransitionType::Honeycomb | TransitionType::Shred |
TransitionType::Switch | TransitionType::Flip | TransitionType::Pan |
TransitionType::Ferris | TransitionType::Gallery | TransitionType::Conveyor |
TransitionType::Doors | TransitionType::Box | TransitionType::Zoom
);
let is_p15 = matches!(&props.transition_type, TransitionType::Morph);
let standard_inner = match &props.transition_type {
TransitionType::None => return String::new(),
TransitionType::Cut => "<p:cut/>".to_string(),
TransitionType::Fade => "<p:fade/>".to_string(),
TransitionType::Push => format!("<p:push{dir_attr}/>"),
TransitionType::Wipe => format!("<p:wipe{dir_attr}/>"),
TransitionType::Cover => format!("<p:cover{dir_attr}/>"),
TransitionType::Uncover=> format!("<p:uncover{dir_attr}/>"),
TransitionType::Random => "<p:random/>".to_string(),
TransitionType::RandomBar => {
let vert = matches!(props.direction, Some(TransitionDir::Up) | Some(TransitionDir::Down));
format!("<p:randomBar dir=\"{}\"/>", if vert { "vert" } else { "horz" })
}
TransitionType::Circle => "<p:circle/>".to_string(),
TransitionType::Diamond => "<p:diamond/>".to_string(),
TransitionType::Wheel => "<p:wheel spokes=\"4\"/>".to_string(),
TransitionType::Checker => {
let vert = matches!(props.direction, Some(TransitionDir::Up) | Some(TransitionDir::Down));
format!("<p:checker dir=\"{}\"/>", if vert { "vert" } else { "horz" })
}
TransitionType::Blinds => {
let vert = matches!(props.direction, Some(TransitionDir::Up) | Some(TransitionDir::Down));
format!("<p:blinds dir=\"{}\"/>", if vert { "vert" } else { "horz" })
}
TransitionType::Strips => format!("<p:strips{dir_attr}/>"),
TransitionType::Plus => "<p:plus/>".to_string(),
TransitionType::Split => {
let orient = if matches!(props.direction, Some(TransitionDir::Up) | Some(TransitionDir::Down)) {
"vert"
} else {
"horz"
};
format!("<p:split orient=\"{orient}\"/>")
}
_ => "<p:fade/>".to_string(),
};
if is_p14 || is_p15 {
let p14_inner = match &props.transition_type {
TransitionType::Flash => "<p14:flash/>".to_string(),
TransitionType::Zoom => format!("<p14:prism{dir_attr}/>"),
TransitionType::Vortex => format!("<p14:vortex{dir_attr}/>"),
TransitionType::Ripple => "<p14:ripple/>".to_string(),
TransitionType::Glitter => format!("<p14:glitter{dir_attr}/>"),
TransitionType::Honeycomb=> "<p14:honeycomb/>".to_string(),
TransitionType::Shred => "<p14:shred/>".to_string(),
TransitionType::Switch => format!("<p14:switch{dir_attr}/>"),
TransitionType::Flip => format!("<p14:flip{dir_attr}/>"),
TransitionType::Pan => format!("<p14:pan{dir_attr}/>"),
TransitionType::Ferris => format!("<p14:ferris{dir_attr}/>"),
TransitionType::Gallery => format!("<p14:gallery{dir_attr}/>"),
TransitionType::Conveyor => format!("<p14:conveyor{dir_attr}/>"),
TransitionType::Doors => format!("<p14:doors{dir_attr}/>"),
TransitionType::Box => format!("<p14:window{dir_attr}/>"),
TransitionType::Morph => "<p15:prstTrans prst=\"morph\"/>".to_string(),
_ => unreachable!(),
};
let ns_req = if is_p15 { "p15" } else { "p14" };
let p14_ns = "xmlns:p14=\"http://schemas.microsoft.com/office/powerpoint/2010/main\"";
let extra_ns = if is_p15 {
" xmlns:p15=\"http://schemas.microsoft.com/office/powerpoint/2012/main\""
} else { "" };
format!(
"<mc:AlternateContent xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\">\
<mc:Choice Requires=\"{ns_req}\">\
<p:transition{attrs} {p14_ns}{extra_ns}>{p14_inner}</p:transition>\
</mc:Choice>\
<mc:Fallback>\
<p:transition{attrs}>{standard_inner}</p:transition>\
</mc:Fallback>\
</mc:AlternateContent>"
)
} else {
format!("<p:transition{attrs}>{inner}</p:transition>", inner = standard_inner)
}
}
fn gen_xml_pattern_fill(pf: &PatternFill) -> String {
let prst = pf.pattern.as_str();
let fg = pf.fg_color.trim_start_matches('#').to_uppercase();
let bg = pf.bg_color.trim_start_matches('#').to_uppercase();
format!(
"<a:pattFill prst=\"{prst}\">\
<a:fgClr><a:srgbClr val=\"{fg}\"/></a:fgClr>\
<a:bgClr><a:srgbClr val=\"{bg}\"/></a:bgClr>\
</a:pattFill>"
)
}
fn gen_xml_shadow(shadow: &crate::types::ShadowProps) -> String {
let blur = val_to_pts(shadow.blur.unwrap_or(8.0));
let offset = val_to_pts(shadow.offset.unwrap_or(4.0));
let angle = (shadow.angle.unwrap_or(270.0) * 60_000.0).round() as i64;
let raw_opacity = shadow.opacity.unwrap_or(0.75);
let norm = if raw_opacity > 1.0 { raw_opacity / 100.0 } else { raw_opacity };
let opacity = (norm.clamp(0.0, 1.0) * 100_000.0).round() as i64;
let color = shadow.color.as_deref().unwrap_or("000000");
let type_tag = match shadow.shadow_type {
ShadowType::Outer => "outerShdw",
ShadowType::Inner => "innerShdw",
ShadowType::None => return String::new(),
};
let outer_attrs = if shadow.shadow_type == ShadowType::Outer {
String::from(" algn=\"bl\"")
} else {
String::new()
};
format!(
"<a:effectLst><a:{type_tag}{outer_attrs} blurRad=\"{blur}\" dist=\"{offset}\" dir=\"{angle}\">\
<a:srgbClr val=\"{color}\"><a:alpha val=\"{opacity}\"/></a:srgbClr>\
</a:{type_tag}></a:effectLst>",
type_tag = type_tag,
outer_attrs = outer_attrs,
blur = blur,
offset = offset,
angle = angle,
color = color,
opacity = opacity,
)
}
fn gen_xml_scene3d(scene: &crate::types::Scene3DProps) -> String {
let cam = &scene.camera;
let lr = &scene.light_rig;
let mut s = String::from("<a:scene3d>");
let fov_attr = cam.fov.map(|f| format!(" fov=\"{f}\"")).unwrap_or_default();
s.push_str(&format!("<a:camera prst=\"{}\"{fov_attr}", cam.preset.as_str()));
if let Some(ref rot) = cam.rotation {
s.push_str(&format!("><a:rot lat=\"{}\" lon=\"{}\" rev=\"{}\"/></a:camera>", rot.lat, rot.lon, rot.rev));
} else {
s.push_str("/>");
}
s.push_str(&format!("<a:lightRig rig=\"{}\" dir=\"{}\"", lr.rig_type.as_str(), lr.direction.as_str()));
if let Some(ref rot) = lr.rotation {
s.push_str(&format!("><a:rot lat=\"{}\" lon=\"{}\" rev=\"{}\"/></a:lightRig>", rot.lat, rot.lon, rot.rev));
} else {
s.push_str("/>");
}
s.push_str("</a:scene3d>");
s
}
fn gen_xml_sp3d(sp3d: &crate::types::Shape3DProps) -> String {
let mut attrs = String::new();
if let Some(h) = sp3d.extrusion_height {
attrs.push_str(&format!(" extrusionH=\"{h}\""));
}
if let Some(w) = sp3d.contour_width {
attrs.push_str(&format!(" contourW=\"{w}\""));
}
if let Some(ref mat) = sp3d.material {
attrs.push_str(&format!(" prstMaterial=\"{}\"", mat.as_str()));
}
let mut s = format!("<a:sp3d{attrs}>");
if let Some(ref bevel) = sp3d.bevel_top {
let w = bevel.width.unwrap_or(76200);
let h = bevel.height.unwrap_or(76200);
s.push_str(&format!("<a:bevelT w=\"{w}\" h=\"{h}\" prst=\"{}\"/>", bevel.preset.as_str()));
}
if let Some(ref bevel) = sp3d.bevel_bottom {
let w = bevel.width.unwrap_or(76200);
let h = bevel.height.unwrap_or(76200);
s.push_str(&format!("<a:bevelB w=\"{w}\" h=\"{h}\" prst=\"{}\"/>", bevel.preset.as_str()));
}
if let Some(ref color) = sp3d.contour_color {
s.push_str(&format!("<a:contourClr><a:srgbClr val=\"{color}\"/></a:contourClr>"));
}
s.push_str("</a:sp3d>");
s
}
fn resolve_position(pos: &crate::types::PositionProps, pres: &Presentation) -> (i64, i64, i64, i64) {
let layout = &pres.layout;
let x = pos.x.as_ref().map(|c| c.to_emu(layout.width)).unwrap_or(EMU);
let y = pos.y.as_ref().map(|c| c.to_emu(layout.height)).unwrap_or(EMU);
let cx = pos.w.as_ref().map(|c| c.to_emu(layout.width))
.unwrap_or_else(|| (layout.width as f64 * 0.75).round() as i64);
let cy = pos.h.as_ref().map(|c| c.to_emu(layout.height)).unwrap_or(EMU);
(x, y, cx, cy)
}
fn build_location_attr(rotate: Option<f64>, flip_h: bool, flip_v: bool) -> String {
let mut s = String::new();
if let Some(r) = rotate {
s.push_str(&format!(" rot=\"{}\"", convert_rotation_degrees(r)));
}
if flip_h { s.push_str(" flipH=\"1\""); }
if flip_v { s.push_str(" flipV=\"1\""); }
s
}
pub fn gen_xml_timing(objects: &[SlideObject]) -> String {
use crate::types::AnimationEffect;
let anims: Vec<(usize, &AnimationEffect)> = objects
.iter()
.enumerate()
.flat_map(|(idx, obj)| {
let empty: &[AnimationEffect] = &[];
let anim_slice: &[AnimationEffect] = match obj {
SlideObject::Text(t) => &t.options.animations,
SlideObject::Shape(s) => &s.options.animations,
SlideObject::Image(i) => &i.options.animations,
SlideObject::Table(t) => &t.options.animations,
SlideObject::Connector(c) => &c.options.animations,
SlideObject::Media(_) => empty,
SlideObject::Group(_) => empty,
};
let spid = idx + 2;
anim_slice.iter().map(move |a| (spid, a))
})
.collect();
if anims.is_empty() { return String::new(); }
use crate::types::AnimationTrigger;
#[derive(Clone, Copy, PartialEq)]
enum StepKind { Click, Auto }
#[derive(Clone, Copy, PartialEq)]
enum ItemKind { Click, With, After }
struct StepItem<'a> {
spid: usize,
anim: &'a AnimationEffect,
item_kind: ItemKind,
cumulative_delay_ms: u32, }
struct Step<'a> {
kind: StepKind,
items: Vec<StepItem<'a>>,
}
const DEFAULT_EFFECT_DUR_MS: u32 = 500;
let mut steps: Vec<Step<'_>> = Vec::new();
let mut click_key_to_idx: Vec<(u32, usize)> = Vec::new();
for (spid, anim) in &anims {
match &anim.trigger {
AnimationTrigger::OnClick => {
match anim.click_group {
None => steps.push(Step {
kind: StepKind::Click,
items: vec![StepItem {
spid: *spid,
anim: *anim,
item_kind: ItemKind::Click,
cumulative_delay_ms: anim.delay_ms.unwrap_or(0),
}],
}),
Some(key) => {
if let Some(&(_, gi)) = click_key_to_idx.iter().find(|(k, _)| *k == key) {
steps[gi].items.push(StepItem {
spid: *spid,
anim: *anim,
item_kind: ItemKind::With,
cumulative_delay_ms: anim.delay_ms.unwrap_or(0),
});
} else {
let gi = steps.len();
click_key_to_idx.push((key, gi));
steps.push(Step {
kind: StepKind::Click,
items: vec![StepItem {
spid: *spid,
anim: *anim,
item_kind: ItemKind::Click,
cumulative_delay_ms: anim.delay_ms.unwrap_or(0),
}],
});
}
}
}
}
AnimationTrigger::WithPrevious | AnimationTrigger::AfterPrevious => {
let item_kind = if matches!(anim.trigger, AnimationTrigger::WithPrevious) {
ItemKind::With
} else {
ItemKind::After
};
let user_delay = anim.delay_ms.unwrap_or(0);
if let Some(last) = steps.last_mut() {
let prev = last.items.last().unwrap();
let cumulative = match item_kind {
ItemKind::With => prev.cumulative_delay_ms + user_delay,
ItemKind::After => prev.cumulative_delay_ms + DEFAULT_EFFECT_DUR_MS + user_delay,
ItemKind::Click => unreachable!(),
};
last.items.push(StepItem {
spid: *spid,
anim: *anim,
item_kind,
cumulative_delay_ms: cumulative,
});
} else {
steps.push(Step {
kind: StepKind::Auto,
items: vec![StepItem {
spid: *spid,
anim: *anim,
item_kind,
cumulative_delay_ms: user_delay,
}],
});
}
}
}
}
let mut s = String::new();
s.push_str("<p:timing><p:tnLst><p:par>");
s.push_str("<p:cTn id=\"1\" dur=\"indefinite\" restart=\"never\" nodeType=\"tmRoot\"><p:childTnLst>");
s.push_str("<p:seq concurrent=\"1\" nextAc=\"seek\">");
s.push_str("<p:cTn id=\"2\" dur=\"indefinite\" nodeType=\"mainSeq\"><p:childTnLst>");
let mut bld_entries: Vec<(usize, usize, bool)> = Vec::new(); let mut ctn_id: usize = 3;
for (grp_idx, step) in steps.iter().enumerate() {
let id_outer = ctn_id;
ctn_id += 1;
let needs_begin_cond = step.kind == StepKind::Auto || step.items.len() > 1;
let begin_cond = if needs_begin_cond {
"<p:cond evt=\"onBegin\" delay=\"0\"><p:tn val=\"2\"/></p:cond>"
} else {
""
};
s.push_str(&format!(
"<p:par><p:cTn id=\"{id_outer}\" fill=\"hold\">\
<p:stCondLst><p:cond delay=\"indefinite\"/>{begin_cond}</p:stCondLst>\
<p:childTnLst>"
));
for (j, item) in step.items.iter().enumerate() {
let StepItem { spid, anim, item_kind, cumulative_delay_ms } = item;
let id_middle = ctn_id; ctn_id += 1;
let id_effect = ctn_id; ctn_id += 1;
let id_set = ctn_id; ctn_id += 1;
let id_fx = ctn_id; ctn_id += 1;
let id_fx2 = ctn_id; ctn_id += 1;
let fx = build_effect_xml(&anim.effect, *spid, id_set, id_fx, id_fx2);
let mut inner_xml = fx.inner;
if let Some(tt) = &anim.text_target {
let bare = format!("<p:spTgt spid=\"{spid}\"/>");
let with_tx = match tt {
crate::types::TextTarget::CharRange { st_idx, end_idx } =>
format!("<p:spTgt spid=\"{spid}\"><p:txEl>\
<p:charRg st=\"{st_idx}\" end=\"{end_idx}\"/>\
</p:txEl></p:spTgt>"),
crate::types::TextTarget::ParaRange { st_idx, end_idx } =>
format!("<p:spTgt spid=\"{spid}\"><p:txEl>\
<p:pRg st=\"{st_idx}\" end=\"{end_idx}\"/>\
</p:txEl></p:spTgt>"),
};
inner_xml = inner_xml.replace(&bare, &with_tx);
}
let node_type = if j == 0 {
match step.kind {
StepKind::Click => "clickEffect",
StepKind::Auto => match item_kind {
ItemKind::With => "withEffect",
ItemKind::After => "afterEffect",
ItemKind::Click => "clickEffect", },
}
} else {
match item_kind {
ItemKind::With => "withEffect",
ItemKind::After => "afterEffect",
ItemKind::Click => "withEffect", }
};
let preset_id = fx.preset_id;
let preset_class = fx.preset_class;
let preset_subtype = fx.preset_subtype;
let iterate_xml = fx.iterate.as_deref().unwrap_or("");
let has_sub_target = anim.text_target.is_some();
bld_entries.push((*spid, grp_idx, has_sub_target));
s.push_str(&format!(
"<p:par><p:cTn id=\"{id_middle}\" fill=\"hold\">\
<p:stCondLst><p:cond delay=\"{cumulative_delay_ms}\"/></p:stCondLst>\
<p:childTnLst>\
<p:par>\
<p:cTn id=\"{id_effect}\" presetID=\"{preset_id}\" presetClass=\"{preset_class}\" \
presetSubtype=\"{preset_subtype}\" fill=\"hold\" grpId=\"{grp_idx}\" nodeType=\"{node_type}\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst>\
{iterate_xml}\
<p:childTnLst>{inner_xml}</p:childTnLst></p:cTn>\
</p:par>\
</p:childTnLst></p:cTn></p:par>"
));
}
s.push_str("</p:childTnLst></p:cTn></p:par>");
}
s.push_str("</p:childTnLst></p:cTn>");
s.push_str("<p:prevCondLst><p:cond evt=\"onPrev\" delay=\"0\"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst>");
s.push_str("<p:nextCondLst><p:cond evt=\"onNext\" delay=\"0\"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst>");
s.push_str("</p:seq></p:childTnLst></p:cTn></p:par></p:tnLst>");
let mut seen: Vec<(usize, usize, bool)> = Vec::new();
for (spid, grp_id, has_sub_target) in &bld_entries {
if let Some(existing) = seen.iter_mut().find(|(s, g, _)| s == spid && g == grp_id) {
existing.2 = existing.2 || *has_sub_target;
} else {
seen.push((*spid, *grp_id, *has_sub_target));
}
}
s.push_str("<p:bldLst>");
for (spid, grp_id, has_sub_target) in &seen {
if *has_sub_target {
s.push_str(&format!("<p:bldP spid=\"{spid}\" grpId=\"{grp_id}\" uiExpand=\"1\"/>"));
} else {
s.push_str(&format!("<p:bldP spid=\"{spid}\" grpId=\"{grp_id}\" animBg=\"1\"/>"));
}
}
s.push_str("</p:bldLst></p:timing>");
s
}
struct EffectXml {
preset_id: u32,
preset_class: &'static str,
preset_subtype: u32,
iterate: Option<String>,
inner: String,
}
impl EffectXml {
fn new(preset_id: u32, preset_class: &'static str, preset_subtype: u32, inner: String) -> Self {
Self { preset_id, preset_class, preset_subtype, iterate: None, inner }
}
}
fn filter_effect(
preset_id: u32, subtype: u32, filter: &str, entering: bool,
spid: usize, id_set: usize, id_fx: usize,
) -> EffectXml {
let class = if entering { "entr" } else { "exit" };
let dir = if entering { "in" } else { "out" };
let inner = if entering {
format!("{}{}", set_visibility_xml(spid, id_set, "visible", 0),
anim_effect_filter(spid, id_fx, dir, filter, 500))
} else {
format!("{}{}", anim_effect_filter(spid, id_fx, dir, filter, 500),
set_visibility_xml(spid, id_set, "hidden", 500))
};
EffectXml::new(preset_id, class, subtype, inner)
}
fn scale_effect(
preset_id: u32, from: u32, to: u32, dur: u32, entering: bool,
spid: usize, id_set: usize, id_fx: usize,
) -> EffectXml {
let class = if entering { "entr" } else { "exit" };
let inner = if entering {
format!("{}{}", set_visibility_xml(spid, id_set, "visible", 0),
anim_scale_xml(spid, id_fx, from, to, dur))
} else {
format!("{}{}", anim_scale_xml(spid, id_fx, from, to, dur),
set_visibility_xml(spid, id_set, "hidden", dur))
};
EffectXml::new(preset_id, class, 0, inner)
}
fn scale_xy_effect(
preset_id: u32, subtype: u32,
from_x: u32, from_y: u32, to_x: u32, to_y: u32, dur: u32,
entering: bool, spid: usize, id_set: usize, id_fx: usize,
) -> EffectXml {
let class = if entering { "entr" } else { "exit" };
let inner = if entering {
format!("{}{}", set_visibility_xml(spid, id_set, "visible", 0),
anim_scale_xy_xml(spid, id_fx, from_x, from_y, to_x, to_y, dur))
} else {
format!("{}{}", anim_scale_xy_xml(spid, id_fx, from_x, from_y, to_x, to_y, dur),
set_visibility_xml(spid, id_set, "hidden", dur))
};
EffectXml::new(preset_id, class, subtype, inner)
}
fn scale_rot_effect(
preset_id: u32, scale_from: u32, scale_to: u32, rot_degrees: f32, dur: u32,
entering: bool, spid: usize, id_set: usize, id_fx: usize, id_fx2: usize,
) -> EffectXml {
let class = if entering { "entr" } else { "exit" };
let rot_by = (rot_degrees * 60_000.0).round() as i64;
let anim = format!("{}{}", anim_scale_xml(spid, id_fx, scale_from, scale_to, dur),
anim_rot_xml(spid, id_fx2, rot_by, dur));
let inner = if entering {
format!("{}{}", set_visibility_xml(spid, id_set, "visible", 0), anim)
} else {
format!("{}{}", anim, set_visibility_xml(spid, id_set, "hidden", dur))
};
EffectXml::new(preset_id, class, 0, inner)
}
fn scale_fly_effect(
preset_id: u32, scale_from: u32, scale_to: u32,
fly_attr: &str, fly_start: f32, fly_end: f32, dur: u32,
entering: bool, spid: usize, id_set: usize, id_fx: usize, id_fx2: usize,
) -> EffectXml {
let class = if entering { "entr" } else { "exit" };
let anim = format!("{}{}", anim_scale_xml(spid, id_fx, scale_from, scale_to, dur),
anim_fly_xml(spid, id_fx2, fly_attr, fly_start, fly_end, dur));
let inner = if entering {
format!("{}{}", set_visibility_xml(spid, id_set, "visible", 0), anim)
} else {
format!("{}{}", anim, set_visibility_xml(spid, id_set, "hidden", dur))
};
EffectXml::new(preset_id, class, 0, inner)
}
fn fly_effect(
preset_id: u32, subtype: u32,
attr: &str, start_val: f32, end_val: f32, dur: u32,
entering: bool, spid: usize, id_set: usize, id_fx: usize,
) -> EffectXml {
let class = if entering { "entr" } else { "exit" };
let inner = if entering {
format!("{}{}", set_visibility_xml(spid, id_set, "visible", 0),
anim_fly_xml(spid, id_fx, attr, start_val, end_val, dur))
} else {
format!("{}{}", anim_fly_xml(spid, id_fx, attr, start_val, end_val, dur),
set_visibility_xml(spid, id_set, "hidden", dur))
};
EffectXml::new(preset_id, class, subtype, inner)
}
fn rot_effect(
preset_id: u32, rot_degrees: f32, dur: u32,
entering: bool, spid: usize, id_set: usize, id_fx: usize,
) -> EffectXml {
let class = if entering { "entr" } else { "exit" };
let rot_by = (rot_degrees * 60_000.0).round() as i64;
let inner = if entering {
format!("{}{}", set_visibility_xml(spid, id_set, "visible", 0),
anim_rot_xml(spid, id_fx, rot_by, dur))
} else {
format!("{}{}", anim_rot_xml(spid, id_fx, rot_by, dur),
set_visibility_xml(spid, id_set, "hidden", dur))
};
EffectXml::new(preset_id, class, 0, inner)
}
fn build_effect_xml(
effect: &AnimationEffectType,
spid: usize,
id_set: usize,
id_fx: usize,
id_fx2: usize,
) -> EffectXml {
match effect {
AnimationEffectType::Appear => EffectXml::new(1, "entr", 0, set_visibility_xml(spid, id_set, "visible", 0)),
AnimationEffectType::Disappear => EffectXml::new(1, "exit", 0, set_visibility_xml(spid, id_set, "hidden", 0)),
AnimationEffectType::FadeIn => filter_effect(10, 0, "fade", true, spid, id_set, id_fx),
AnimationEffectType::FadeOut => filter_effect(10, 0, "fade", false, spid, id_set, id_fx),
AnimationEffectType::WipeIn(dir) => {
let (subtype, filter) = wipe_filter(dir);
filter_effect(4, subtype, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::WipeOut(dir) => {
let (subtype, filter) = wipe_filter(dir);
filter_effect(4, subtype, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::SplitIn(orient) => {
let (subtype, filter) = split_filter(orient, true);
filter_effect(3, subtype, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::SplitOut(orient) => {
let (subtype, filter) = split_filter(orient, false);
filter_effect(3, subtype, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::ZoomIn => scale_effect(11, 10000, 100000, 500, true, spid, id_set, id_fx),
AnimationEffectType::ZoomOut => scale_effect(11, 100000, 10000, 500, false, spid, id_set, id_fx),
AnimationEffectType::FlyIn(dir) => {
let (subtype, attr, start_val, end_val) = fly_params(dir, true);
fly_effect(2, subtype, attr, start_val, end_val, 500, true, spid, id_set, id_fx)
}
AnimationEffectType::FlyOut(dir) => {
let (subtype, attr, start_val, end_val) = fly_params(dir, false);
fly_effect(2, subtype, attr, start_val, end_val, 500, false, spid, id_set, id_fx)
}
AnimationEffectType::BlindsIn(orient) => {
let (subtype, filter) = blinds_filter(orient);
filter_effect(3, subtype, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::BlindsOut(orient) => {
let (subtype, filter) = blinds_filter(orient);
filter_effect(3, subtype, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::CheckerboardIn(dir) => {
let (subtype, filter) = checkerboard_filter(dir);
filter_effect(5, subtype, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::CheckerboardOut(dir) => {
let (subtype, filter) = checkerboard_filter(dir);
filter_effect(5, subtype, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::DissolveIn => filter_effect(12, 0, "dissolve()", true, spid, id_set, id_fx),
AnimationEffectType::DissolveOut => filter_effect(12, 0, "dissolve()", false, spid, id_set, id_fx),
AnimationEffectType::PeekIn(dir) => {
let (subtype, filter) = wipe_filter(dir);
filter_effect(13, subtype, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::PeekOut(dir) => {
let (subtype, filter) = wipe_filter(dir);
filter_effect(13, subtype, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::RandomBarsIn(orient) => {
let (subtype, filter) = random_bars_filter(orient);
filter_effect(14, subtype, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::RandomBarsOut(orient) => {
let (subtype, filter) = random_bars_filter(orient);
filter_effect(14, subtype, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::ShapeIn(variant) => {
let (pid, filter) = shape_filter(variant, true);
filter_effect(pid, 0, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::ShapeOut(variant) => {
let (pid, filter) = shape_filter(variant, false);
filter_effect(pid, 0, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::StripsIn(dir) => {
let (subtype, filter) = strips_filter(dir);
filter_effect(6, subtype, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::StripsOut(dir) => {
let (subtype, filter) = strips_filter(dir);
filter_effect(6, subtype, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::WedgeIn => filter_effect(17, 0, "wedge()", true, spid, id_set, id_fx),
AnimationEffectType::WedgeOut => filter_effect(17, 0, "wedge()", false, spid, id_set, id_fx),
AnimationEffectType::WheelIn(spokes) => {
let n = (*spokes).max(1);
let filter = format!("wheel(spokes={n})");
filter_effect(18, n, &filter, true, spid, id_set, id_fx)
}
AnimationEffectType::WheelOut(spokes) => {
let n = (*spokes).max(1);
let filter = format!("wheel(spokes={n})");
filter_effect(18, n, &filter, false, spid, id_set, id_fx)
}
AnimationEffectType::ExpandIn => scale_effect(22, 0, 100000, 500, true, spid, id_set, id_fx),
AnimationEffectType::ContractOut => scale_effect(22, 100000, 0, 500, false, spid, id_set, id_fx),
AnimationEffectType::SwivelIn => scale_xy_effect(21, 0, 0, 100000, 100000, 100000, 500, true, spid, id_set, id_fx),
AnimationEffectType::SwivelOut => scale_xy_effect(21, 0, 100000, 100000, 0, 100000, 500, false, spid, id_set, id_fx),
AnimationEffectType::BasicZoomIn => scale_effect(27, 10000, 100000, 500, true, spid, id_set, id_fx),
AnimationEffectType::BasicZoomOut => scale_effect(27, 100000, 10000, 500, false, spid, id_set, id_fx),
AnimationEffectType::CentreRevolveIn => scale_rot_effect(23, 10000, 100000, 720.0, 700, true, spid, id_set, id_fx, id_fx2),
AnimationEffectType::CentreRevolveOut => scale_rot_effect(23, 100000, 10000, 720.0, 700, false, spid, id_set, id_fx, id_fx2),
AnimationEffectType::FloatIn(_dir) => {
let inner = format!("{}{}{}",
set_visibility_xml(spid, id_set, "visible", 0),
anim_effect_filter(spid, id_fx, "in", "fade", 800),
anim_style_rotation_xml(spid, id_fx2, -90.0, 0.0, 800));
EffectXml::new(30, "entr", 0, inner)
}
AnimationEffectType::FloatOut(_dir) => {
let inner = format!("{}{}{}",
anim_effect_filter(spid, id_fx, "out", "fade", 800),
anim_style_rotation_xml(spid, id_fx2, 0.0, 90.0, 800),
set_visibility_xml(spid, id_set, "hidden", 800));
EffectXml::new(30, "exit", 0, inner)
}
AnimationEffectType::GrowTurnIn => scale_rot_effect(24, 10000, 100000, 90.0, 500, true, spid, id_set, id_fx, id_fx2),
AnimationEffectType::ShrinkTurnOut => scale_rot_effect(24, 100000, 10000, 90.0, 500, false, spid, id_set, id_fx, id_fx2),
AnimationEffectType::RiseUpIn => fly_effect(25, 0, "ppt_y", 0.5, 0.0, 500, true, spid, id_set, id_fx),
AnimationEffectType::SinkDownOut => fly_effect(25, 0, "ppt_y", 0.0, 0.5, 500, false, spid, id_set, id_fx),
AnimationEffectType::SpinnerIn => scale_rot_effect(28, 10000, 100000, 360.0, 700, true, spid, id_set, id_fx, id_fx2),
AnimationEffectType::SpinnerOut => scale_rot_effect(28, 100000, 10000, 360.0, 700, false, spid, id_set, id_fx, id_fx2),
AnimationEffectType::StretchIn(dir) => {
let (from_x, from_y, subtype) = match dir {
Direction::Left | Direction::Right => (0, 100000, 4u32),
Direction::Up | Direction::Down => (100000, 0, 8u32),
};
scale_xy_effect(29, subtype, from_x, from_y, 100000, 100000, 500, true, spid, id_set, id_fx)
}
AnimationEffectType::StretchyOut(dir) => {
let (to_x, to_y, subtype) = match dir {
Direction::Left | Direction::Right => (0, 100000, 4u32),
Direction::Up | Direction::Down => (100000, 0, 8u32),
};
scale_xy_effect(29, subtype, 100000, 100000, to_x, to_y, 500, false, spid, id_set, id_fx)
}
AnimationEffectType::BoomerangIn => scale_fly_effect(36, 10000, 100000, "ppt_x", 1.0, 0.0, 700, true, spid, id_set, id_fx, id_fx2),
AnimationEffectType::BoomerangOut => scale_fly_effect(36, 100000, 10000, "ppt_x", 0.0, 1.0, 700, false, spid, id_set, id_fx, id_fx2),
AnimationEffectType::BounceIn => {
let bounce_y = anim_keyframes_xml(spid, id_fx2, "ppt_y", &[
(0.0, -1.5), (0.365, 0.08), (0.55, -0.04),
(0.72, 0.015), (0.85, -0.005), (1.0, 0.0),
], 1600);
let inner = format!("{}{}{}",
set_visibility_xml(spid, id_set, "visible", 0),
anim_effect_filter(spid, id_fx, "in", "wipe(down)", 580),
bounce_y);
EffectXml::new(26, "entr", 0, inner)
}
AnimationEffectType::BounceOut => {
let bounce_y = anim_keyframes_xml(spid, id_fx, "ppt_y", &[
(0.0, 0.0), (0.15, -0.015), (0.30, 0.005),
(0.45, -0.003), (0.55, 0.0), (1.0, 1.5),
], 1600);
let inner = format!("{}{}", bounce_y, set_visibility_xml(spid, id_set, "hidden", 1600));
EffectXml::new(26, "exit", 0, inner)
}
AnimationEffectType::CreditsIn => fly_effect(32, 0, "ppt_y", 1.0, 0.0, 2000, true, spid, id_set, id_fx),
AnimationEffectType::CreditsOut => fly_effect(32, 0, "ppt_y", 0.0, -1.0, 2000, false, spid, id_set, id_fx),
AnimationEffectType::CurveUpIn => {
let id_fx3 = id_fx2 + 1;
let path = "M -0.46736 0.92887 C -0.37517 0.88508 -0.02552 0.75279 \
0.0908 0.66613 C 0.20747 0.57948 0.21649 0.50394 \
0.23177 0.40825 C 0.24705 0.31256 0.22118 0.15964 \
0.18264 0.09152 C 0.1441 0.02341 0.03802 0.0 0.0 0.0";
let inner = format!("{}{}{}{}",
set_visibility_xml(spid, id_set, "visible", 0),
anim_scale_decel_xy_xml(spid, id_fx, 250000, 250000, 100000, 100000, 1000, 50000),
anim_motion_xml(spid, id_fx2, path, 1000, 50000),
anim_effect_filter(spid, id_fx3, "in", "fade", 1000));
EffectXml::new(52, "entr", 0, inner)
}
AnimationEffectType::CurveDownOut => {
let id_fx3 = id_fx2 + 1;
let path = "M 0.0 0.0 C 0.03802 0.0 0.1441 0.02341 0.18264 0.09152 \
C 0.22118 0.15964 0.24705 0.31256 0.23177 0.40825 \
C 0.21649 0.50394 0.20747 0.57948 0.0908 0.66613 \
C -0.02552 0.75279 -0.37517 0.88508 -0.46736 0.92887";
let inner = format!("{}{}{}{}",
anim_scale_decel_xy_xml(spid, id_fx, 100000, 100000, 250000, 250000, 1000, 50000),
anim_motion_xml(spid, id_fx2, path, 1000, 50000),
anim_effect_filter(spid, id_fx3, "out", "fade", 1000),
set_visibility_xml(spid, id_set, "hidden", 1000));
EffectXml::new(52, "exit", 0, inner)
}
AnimationEffectType::DropIn => fly_effect(34, 0, "ppt_y", -1.0, 0.0, 500, true, spid, id_set, id_fx),
AnimationEffectType::DropOut => fly_effect(34, 0, "ppt_y", 0.0, 1.0, 500, false, spid, id_set, id_fx),
AnimationEffectType::FlipIn => scale_xy_effect(35, 0, 0, 100000, 100000, 100000, 500, true, spid, id_set, id_fx),
AnimationEffectType::FlipOut => scale_xy_effect(35, 0, 100000, 100000, 0, 100000, 500, false, spid, id_set, id_fx),
AnimationEffectType::CollapseOut =>
scale_xy_effect(31, 0, 100000, 100000, 100000, 0, 500, false, spid, id_set, id_fx),
AnimationEffectType::PinwheelIn => scale_rot_effect(37, 10000, 100000, 720.0, 600, true, spid, id_set, id_fx, id_fx2),
AnimationEffectType::PinwheelOut => scale_rot_effect(37, 100000, 10000, 720.0, 600, false, spid, id_set, id_fx, id_fx2),
AnimationEffectType::SpiralIn => scale_fly_effect(38, 10000, 100000, "ppt_x", -0.5, 0.0, 700, true, spid, id_set, id_fx, id_fx2),
AnimationEffectType::SpiralOut => scale_fly_effect(38, 100000, 10000, "ppt_x", 0.0, 0.5, 700, false, spid, id_set, id_fx, id_fx2),
AnimationEffectType::BasicSwivelIn => rot_effect(39, 90.0, 500, true, spid, id_set, id_fx),
AnimationEffectType::BasicSwivelOut => rot_effect(39, 90.0, 500, false, spid, id_set, id_fx),
AnimationEffectType::WhipIn => scale_fly_effect(40, 10000, 100000, "ppt_x", 1.0, 0.0, 400, true, spid, id_set, id_fx, id_fx2),
AnimationEffectType::WhipOut => scale_fly_effect(40, 100000, 10000, "ppt_x", 0.0, 1.0, 400, false, spid, id_set, id_fx, id_fx2),
AnimationEffectType::Spin(degrees) => {
let by = (*degrees * 60_000.0).round() as i64;
EffectXml::new(8, "emph", 0, anim_rot_xml(spid, id_fx, by, 2000))
}
AnimationEffectType::Pulse =>
EffectXml::new(14, "emph", 0, anim_pulse_xml(spid, id_fx, 500)),
AnimationEffectType::GrowShrink(scale) => {
let to_val = (*scale * 100_000.0).round() as u32;
EffectXml::new(18, "emph", 0, anim_scale_xml(spid, id_fx, 100000, to_val, 500))
}
AnimationEffectType::FillColor(hex) =>
EffectXml::new(1, "emph", 0, anim_clr_to_xml(spid, id_fx, "fillcolor", hex, 500, false)),
AnimationEffectType::FontColor(hex) =>
EffectXml::new(2, "emph", 0, anim_clr_to_xml(spid, id_fx, "style.color", hex, 500, false)),
AnimationEffectType::LineColor(hex) =>
EffectXml::new(7, "emph", 2, format!("{}{}",
anim_clr_to_xml(spid, id_fx, "stroke.color", hex, 2000, false),
format!("<p:set><p:cBhvr>\
<p:cTn id=\"{id_fx2}\" dur=\"2000\" fill=\"hold\"/>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>stroke.on</p:attrName></p:attrNameLst>\
</p:cBhvr><p:to><p:strVal val=\"true\"/></p:to></p:set>"))),
AnimationEffectType::Transparency(level) => {
let val = level.clamp(0.0, 1.0);
EffectXml::new(4, "emph", 0, anim_opacity_xml(spid, id_fx, val, 500, true))
}
AnimationEffectType::BoldFlash =>
EffectXml::new(10, "emph", 0, format!("\
<p:anim calcmode=\"discrete\" valueType=\"str\">\
<p:cBhvr override=\"childStyle\">\
<p:cTn id=\"{id_fx}\" dur=\"2000\" fill=\"hold\"/>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>style.fontWeight</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:tavLst>\
<p:tav tm=\"0\"><p:val><p:strVal val=\"normal\"/></p:val></p:tav>\
<p:tav tm=\"50000\"><p:val><p:strVal val=\"bold\"/></p:val></p:tav>\
<p:tav tm=\"60000\"><p:val><p:strVal val=\"normal\"/></p:val></p:tav>\
<p:tav tm=\"100000\"><p:val><p:strVal val=\"normal\"/></p:val></p:tav>\
</p:tavLst></p:anim>")),
AnimationEffectType::BrushColor(hex) =>
EffectXml::new(6, "emph", 0, anim_clr_to_xml(spid, id_fx, "fillcolor", hex, 500, true)),
AnimationEffectType::ComplementaryColor =>
EffectXml::new(7, "emph", 0,
anim_clr_hsl_by_xml(spid, id_fx, "fillcolor", 10_800_000, 0, 0, 500, true)),
AnimationEffectType::ComplementaryColor2 =>
EffectXml::new(9, "emph", 0,
anim_clr_hsl_by_xml(spid, id_fx, "fillcolor", 7_200_000, 0, 0, 500, true)),
AnimationEffectType::ContrastingColor =>
EffectXml::new(10, "emph", 0,
anim_clr_hsl_by_xml(spid, id_fx, "fillcolor", 0, 0, 50_000, 500, true)),
AnimationEffectType::Darken =>
EffectXml::new(11, "emph", 0, anim_opacity_xml(spid, id_fx, 0.55, 500, true)),
AnimationEffectType::Desaturate =>
EffectXml::new(12, "emph", 0, anim_clr_to_xml(spid, id_fx, "fillcolor", "808080", 500, true)),
AnimationEffectType::Lighten =>
EffectXml::new(13, "emph", 0, anim_clr_to_xml(spid, id_fx, "fillcolor", "E8E8E8", 500, true)),
AnimationEffectType::ObjectColor(hex) =>
EffectXml::new(15, "emph", 0, anim_clr_to_xml(spid, id_fx, "fillcolor", hex, 500, true)),
AnimationEffectType::Underline =>
EffectXml::new(16, "emph", 0,
anim_str_discrete_xml(spid, id_fx, "style.textDecoration", "none", "underline", 500, true)),
AnimationEffectType::ColorPulse(hex) =>
EffectXml::new(17, "emph", 0, anim_clr_to_xml(spid, id_fx, "fillcolor", hex, 500, true)),
AnimationEffectType::GrowWithColor(hex) => {
EffectXml::new(19, "emph", 0,
format!("{}{}",
anim_scale_xml(spid, id_fx, 100000, 115000, 500),
anim_clr_to_xml(spid, id_fx2, "fillcolor", hex, 500, false)))
}
AnimationEffectType::Shimmer =>
EffectXml {
preset_id: 36,
preset_class: "emph",
preset_subtype: 0,
iterate: Some("<p:iterate type=\"lt\"><p:tmPct val=\"10000\"/></p:iterate>".to_string()),
inner: format!("\
<p:animScale>\
<p:cBhvr>\
<p:cTn id=\"{id_fx}\" dur=\"250\" autoRev=\"1\" fill=\"hold\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
</p:cBhvr>\
<p:to x=\"80000\" y=\"100000\"/>\
</p:animScale>\
<p:anim by=\"(#ppt_w*0.10)\" calcmode=\"lin\" valueType=\"num\">\
<p:cBhvr>\
<p:cTn id=\"{id_fx2}\" dur=\"250\" autoRev=\"1\" fill=\"hold\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>ppt_x</p:attrName></p:attrNameLst>\
</p:cBhvr></p:anim>\
<p:animRot by=\"-480000\">\
<p:cBhvr>\
<p:cTn id=\"{}\" dur=\"250\" autoRev=\"1\" fill=\"hold\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>r</p:attrName></p:attrNameLst>\
</p:cBhvr></p:animRot>", id_fx2 + 1),
},
AnimationEffectType::Teeter =>
EffectXml::new(21, "emph", 0, anim_keyframes_xml(spid, id_fx, "style.rotation", &[
(0.0, 0.0), (0.2, 4.0), (0.4, -4.0), (0.6, 4.0), (0.8, -4.0), (1.0, 0.0),
], 700)),
AnimationEffectType::Blink =>
EffectXml::new(22, "emph", 0, format!("\
<p:anim calcmode=\"discrete\" valueType=\"str\">\
<p:cBhvr>\
<p:cTn id=\"{id_fx}\" dur=\"750\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:tavLst>\
<p:tav tm=\"0\"><p:val><p:strVal val=\"visible\"/></p:val></p:tav>\
<p:tav tm=\"16667\"><p:val><p:strVal val=\"hidden\"/></p:val></p:tav>\
<p:tav tm=\"33333\"><p:val><p:strVal val=\"visible\"/></p:val></p:tav>\
<p:tav tm=\"50000\"><p:val><p:strVal val=\"hidden\"/></p:val></p:tav>\
<p:tav tm=\"66667\"><p:val><p:strVal val=\"visible\"/></p:val></p:tav>\
<p:tav tm=\"83333\"><p:val><p:strVal val=\"hidden\"/></p:val></p:tav>\
<p:tav tm=\"100000\"><p:val><p:strVal val=\"visible\"/></p:val></p:tav>\
</p:tavLst></p:anim>")),
AnimationEffectType::BoldReveal =>
EffectXml {
preset_id: 15,
preset_class: "emph",
preset_subtype: 0,
iterate: Some("<p:iterate type=\"lt\"><p:tmAbs val=\"25\"/></p:iterate>".to_string()),
inner: format!("<p:set>\
<p:cBhvr override=\"childStyle\">\
<p:cTn id=\"{id_fx}\" dur=\"indefinite\"/>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>style.fontWeight</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:to><p:strVal val=\"bold\"/></p:to></p:set>"),
},
AnimationEffectType::Wave =>
EffectXml::new(24, "emph", 0, anim_keyframes_xml(spid, id_fx, "style.rotation", &[
(0.0, 0.0), (0.1, -5.0), (0.2, 5.0), (0.3, -5.0), (0.4, 5.0),
(0.5, -3.0), (0.6, 3.0), (0.7, -2.0), (0.8, 2.0), (1.0, 0.0),
], 1000)),
}
}
fn set_visibility_xml(spid: usize, id: usize, val: &str, delay_ms: u32) -> String {
format!(
"<p:set><p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"1\" fill=\"hold\">\
<p:stCondLst><p:cond delay=\"{delay_ms}\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst>\
</p:cBhvr><p:to><p:strVal val=\"{val}\"/></p:to></p:set>"
)
}
fn anim_effect_filter(spid: usize, id: usize, transition: &str, filter: &str, dur: u32) -> String {
format!(
"<p:animEffect transition=\"{transition}\" filter=\"{filter}\">\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
</p:cBhvr></p:animEffect>"
)
}
fn anim_scale_xy_xml(
spid: usize, id: usize,
from_x: u32, from_y: u32,
to_x: u32, to_y: u32,
dur: u32,
) -> String {
format!(
"<p:animScale>\
<p:cBhvr><p:cTn id=\"{id}\" dur=\"{dur}\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl></p:cBhvr>\
<p:from x=\"{from_x}\" y=\"{from_y}\"/>\
<p:to x=\"{to_x}\" y=\"{to_y}\"/>\
</p:animScale>"
)
}
fn anim_scale_xml(spid: usize, id: usize, from_val: u32, to_val: u32, dur: u32) -> String {
format!(
"<p:animScale>\
<p:cBhvr><p:cTn id=\"{id}\" dur=\"{dur}\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl></p:cBhvr>\
<p:from x=\"{from_val}\" y=\"{from_val}\"/>\
<p:to x=\"{to_val}\" y=\"{to_val}\"/>\
</p:animScale>"
)
}
fn anim_pulse_xml(spid: usize, id: usize, dur: u32) -> String {
let half = dur / 2;
format!(
"<p:animScale>\
<p:cBhvr><p:cTn id=\"{id}\" dur=\"{half}\" autoRev=\"1\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl></p:cBhvr>\
<p:to x=\"115000\" y=\"115000\"/>\
</p:animScale>"
)
}
fn anim_fly_xml(
spid: usize, id: usize,
attr: &str, start_val: f32, end_val: f32,
dur: u32,
) -> String {
format!(
"<p:anim calcmode=\"lin\" valueType=\"num\">\
<p:cBhvr additive=\"sum\">\
<p:cTn id=\"{id}\" dur=\"{dur}\" fill=\"hold\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>{attr}</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:tavLst>\
<p:tav tm=\"0\"><p:val><p:fltVal val=\"{start_val}\"/></p:val></p:tav>\
<p:tav tm=\"100000\"><p:val><p:fltVal val=\"{end_val}\"/></p:val></p:tav>\
</p:tavLst></p:anim>"
)
}
fn anim_style_rotation_xml(spid: usize, id: usize, from_deg: f32, to_deg: f32, dur: u32) -> String {
format!(
"<p:anim calcmode=\"lin\" valueType=\"num\">\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\" decel=\"100000\" fill=\"hold\"/>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>style.rotation</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:tavLst>\
<p:tav tm=\"0\"><p:val><p:fltVal val=\"{from_deg}\"/></p:val></p:tav>\
<p:tav tm=\"100000\"><p:val><p:fltVal val=\"{to_deg}\"/></p:val></p:tav>\
</p:tavLst></p:anim>"
)
}
fn anim_rot_xml(spid: usize, id: usize, by_units: i64, dur: u32) -> String {
format!(
"<p:animRot by=\"{by_units}\">\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\" fill=\"hold\"/>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>r</p:attrName></p:attrNameLst>\
</p:cBhvr></p:animRot>"
)
}
fn anim_clr_to_xml(spid: usize, id: usize, attr: &str, hex: &str, dur: u32, auto_rev: bool) -> String {
let ar = if auto_rev { " autoRev=\"1\"" } else { "" };
format!("<p:animClr clrSpc=\"rgb\" dir=\"cw\">\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\"{ar} fill=\"hold\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>{attr}</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:to><a:srgbClr val=\"{hex}\"/></p:to>\
</p:animClr>")
}
fn anim_clr_hsl_by_xml(spid: usize, id: usize, attr: &str,
h: i64, s: i64, l: i64,
dur: u32, auto_rev: bool) -> String {
let ar = if auto_rev { " autoRev=\"1\"" } else { "" };
format!("<p:animClr clrSpc=\"hsl\" dir=\"cw\">\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\"{ar} fill=\"hold\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>{attr}</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:by><p:hsl h=\"{h}\" s=\"{s}\" l=\"{l}\"/></p:by>\
</p:animClr>")
}
fn anim_opacity_xml(spid: usize, id: usize, target: f32, dur: u32, auto_rev: bool) -> String {
let ar = if auto_rev { " autoRev=\"1\"" } else { "" };
format!("<p:anim calcmode=\"lin\" valueType=\"num\">\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\"{ar}>\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>style.opacity</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:tavLst>\
<p:tav tm=\"0\"><p:val><p:fltVal val=\"1\"/></p:val></p:tav>\
<p:tav tm=\"100000\"><p:val><p:fltVal val=\"{target}\"/></p:val></p:tav>\
</p:tavLst></p:anim>")
}
fn anim_str_discrete_xml(spid: usize, id: usize, attr: &str,
from_val: &str, to_val: &str,
dur: u32, auto_rev: bool) -> String {
let ar = if auto_rev { " autoRev=\"1\"" } else { "" };
format!("<p:anim calcmode=\"discrete\" valueType=\"str\">\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\"{ar}>\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>{attr}</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:tavLst>\
<p:tav tm=\"0\"><p:val><p:strVal val=\"{from_val}\"/></p:val></p:tav>\
<p:tav tm=\"100000\"><p:val><p:strVal val=\"{to_val}\"/></p:val></p:tav>\
</p:tavLst></p:anim>")
}
fn anim_keyframes_xml(spid: usize, id: usize, attr: &str, keyframes: &[(f32, f32)], dur: u32) -> String {
let tav_list: String = keyframes.iter().map(|(t, v)| {
let tm = (*t * 100_000.0).round() as u32;
format!("<p:tav tm=\"{tm}\"><p:val><p:fltVal val=\"{v}\"/></p:val></p:tav>")
}).collect();
format!("<p:anim calcmode=\"lin\" valueType=\"num\">\
<p:cBhvr additive=\"sum\">\
<p:cTn id=\"{id}\" dur=\"{dur}\" fill=\"hold\">\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
<p:attrNameLst><p:attrName>{attr}</p:attrName></p:attrNameLst>\
</p:cBhvr>\
<p:tavLst>{tav_list}</p:tavLst>\
</p:anim>")
}
fn anim_motion_xml(spid: usize, id: usize, path: &str, dur: u32, decelerate: u32) -> String {
let decel_attr = if decelerate > 0 { format!(" decelerate=\"{decelerate}\"") } else { String::new() };
format!("<p:animMotion origin=\"layout\" path=\"{path}\" pathEditMode=\"relative\">\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\"{decel_attr}>\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
</p:cBhvr>\
</p:animMotion>")
}
fn anim_scale_decel_xy_xml(
spid: usize, id: usize,
from_x: u32, from_y: u32,
to_x: u32, to_y: u32,
dur: u32, decelerate: u32,
) -> String {
let decel_attr = if decelerate > 0 { format!(" decelerate=\"{decelerate}\"") } else { String::new() };
format!("<p:animScale>\
<p:cBhvr>\
<p:cTn id=\"{id}\" dur=\"{dur}\"{decel_attr}>\
<p:stCondLst><p:cond delay=\"0\"/></p:stCondLst></p:cTn>\
<p:tgtEl><p:spTgt spid=\"{spid}\"/></p:tgtEl>\
</p:cBhvr>\
<p:from x=\"{from_x}\" y=\"{from_y}\"/>\
<p:to x=\"{to_x}\" y=\"{to_y}\"/>\
</p:animScale>")
}
fn blinds_filter(orient: &SplitOrientation) -> (u32, String) {
match orient {
SplitOrientation::Horizontal => (10, "blinds(horizontal)".to_string()),
SplitOrientation::Vertical => (4, "blinds(vertical)".to_string()),
}
}
fn checkerboard_filter(dir: &CheckerboardDir) -> (u32, String) {
match dir {
CheckerboardDir::Across => (10, "checkerboard(across)".to_string()),
CheckerboardDir::Down => (4, "checkerboard(down)".to_string()),
}
}
fn random_bars_filter(orient: &SplitOrientation) -> (u32, String) {
match orient {
SplitOrientation::Horizontal => (10, "randombar(horizontal)".to_string()),
SplitOrientation::Vertical => (4, "randombar(vertical)".to_string()),
}
}
fn shape_filter(variant: &ShapeVariant, entering: bool) -> (u32, String) {
let dir = if entering { "in" } else { "out" };
match variant {
ShapeVariant::Box => (8, format!("box({dir})")),
ShapeVariant::Circle => (14, format!("circle({dir})")),
ShapeVariant::Diamond => (15, format!("diamond({dir})")),
ShapeVariant::Plus => (16, format!("plus({dir})")),
}
}
fn strips_filter(dir: &StripDir) -> (u32, String) {
match dir {
StripDir::LeftDown => (9, "strips(leftdown)".to_string()),
StripDir::LeftUp => (3, "strips(leftup)".to_string()),
StripDir::RightDown => (12, "strips(rightdown)".to_string()),
StripDir::RightUp => (6, "strips(rightup)".to_string()),
}
}
fn wipe_filter(dir: &Direction) -> (u32, String) {
let (subtype, name) = match dir {
Direction::Left => (12, "wipe(left)"),
Direction::Right => (4, "wipe(right)"),
Direction::Up => (8, "wipe(up)"),
Direction::Down => (0, "wipe(down)"),
};
(subtype, name.to_string())
}
fn split_filter(orient: &SplitOrientation, entering: bool) -> (u32, String) {
let direction = if entering { "in" } else { "out" };
let (subtype, orientation) = match orient {
SplitOrientation::Horizontal => (22, "horizontal"),
SplitOrientation::Vertical => (24, "vertical"),
};
(subtype, format!("barn(direction={direction},orientation={orientation})"))
}
fn fly_params(dir: &Direction, entering: bool) -> (u32, &'static str, f32, f32) {
match (dir, entering) {
(Direction::Left, true) => (4, "ppt_x", -1.0, 0.0),
(Direction::Right, true) => (2, "ppt_x", 1.0, 0.0),
(Direction::Up, true) => (4, "ppt_y", -1.0, 0.0),
(Direction::Down, true) => (8, "ppt_y", 1.0, 0.0),
(Direction::Left, false) => (4, "ppt_x", 0.0, -1.0),
(Direction::Right, false) => (2, "ppt_x", 0.0, 1.0),
(Direction::Up, false) => (4, "ppt_y", 0.0, -1.0),
(Direction::Down, false) => (8, "ppt_y", 0.0, 1.0),
}
}