ppt-rs 0.2.25

Create, read, and update PowerPoint 2007+ (.pptx) files with rich formatting, bullet styles, themes, and templates.
Documentation
//! Slide layout part XML (`ppt/slideLayouts/slideLayoutN.xml`) with placeholders.

use crate::core::append_usize;
use crate::core::escape_xml;
use crate::generator::slide_content::print_settings::PrintSettings;

/// Number of standard layouts emitted on slide master 1.
pub const STANDARD_LAYOUT_COUNT: usize = 7;

#[allow(clippy::too_many_arguments)]
fn placeholder(id: u32, name: &str, ph_type: &str, ph_idx: Option<u32>, x: u32, y: u32, cx: u32, cy: u32) -> String {
    let idx_xml = ph_idx
        .map(|i| format!(r#" idx="{i}""#))
        .unwrap_or_default();
    format!(
        r#"<p:sp><p:nvSpPr><p:cNvPr id="{id}" name="{name}"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="{ph_type}"{idx_xml}/></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><a:noFill/></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp>"#
    )
}

fn layout_shell(type_attr: &str, name: &str, shapes: &str) -> String {
    format!(
        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="{type_attr}" preserve="1">
<p:cSld name="{name}">
<p:spTree>
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
<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>
{shapes}
</p:spTree>
</p:cSld>
<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
<p:hf/>
</p:sldLayout>"#
    )
}

/// Footer/header/date/slide-number placeholders on the slide master.
pub fn slide_master_footer_shapes(print: Option<&PrintSettings>) -> String {
    let Some(p) = print else {
        return String::new();
    };
    let mut xml = String::new();
    let mut id = 2u32;

    if let Some(ref header) = p.header {
        xml.push_str(&placeholder(
            id,
            "Header Placeholder",
            "hdr",
            None,
            312_420,
            213_360,
            8_519_760,
            365_125,
        ));
        id += 1;
        let _ = header; // placeholder inherits text via slide; fixed text optional in master
    }

    if let Some(ref footer) = p.footer {
        xml.push_str(&format!(
            r#"<p:sp><p:nvSpPr><p:cNvPr id="{id}" name="Footer Placeholder"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="ftr" sz="quarter"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="3124200" y="6356350"/><a:ext cx="2895600" cy="365125"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="en-US" sz="1200"/><a:t>{}</a:t></a:r></a:p></p:txBody></p:sp>"#,
            escape_xml(footer)
        ));
        id += 1;
    }

    if p.print_date {
        xml.push_str(&format!(
            r#"<p:sp><p:nvSpPr><p:cNvPr id="{id}" name="Date Placeholder"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="dt" sz="half"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="457200" y="6356350"/><a:ext cx="2133600" cy="365125"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:fld id="{{B6F15528-F159-4107-2D14-000000000000}}" type="datetimeFigureOut"><a:rPr lang="en-US" sz="1200"/><a:t>6/1/2026</a:t></a:fld><a:endParaRPr lang="en-US" sz="1200"/></a:p></p:txBody></p:sp>"#
        ));
        id += 1;
    }

    if p.print_page_numbers {
        xml.push_str(&format!(
            r#"<p:sp><p:nvSpPr><p:cNvPr id="{id}" name="Slide Number Placeholder"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldNum" sz="quarter"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="8610600" y="6356350"/><a:ext cx="533400" cy="365125"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:fld id="{{B6F15528-F159-4107-2D14-000000000001}}" type="slidenum"><a:rPr lang="en-US" sz="1200"/><a:t>‹#›</a:t></a:fld><a:endParaRPr lang="en-US" sz="1200"/></a:p></p:txBody></p:sp>"#
        ));
    }

    xml
}

/// Generate layout XML for layout index `n` (1-based).
pub fn create_slide_layout_xml(n: usize, _print: Option<&PrintSettings>) -> String {
    match n {
        1 => layout_shell(
            "title",
            "Title Slide",
            &format!(
                "{}{}",
                placeholder(2, "Title", "ctrTitle", None, 1_524_000, 1_828_800, 6_096_000, 1_828_800),
                placeholder(3, "Subtitle", "subTitle", None, 1_524_000, 3_657_600, 6_096_000, 914_400),
            ),
        ),
        2 => layout_shell(
            "obj",
            "Title and Content",
            &format!(
                "{}{}",
                placeholder(2, "Title", "title", None, 457_200, 274_638, 8_229_600, 1_143_000),
                placeholder(3, "Content", "body", Some(1), 457_200, 1_600_200, 8_229_600, 4_525_963),
            ),
        ),
        3 => layout_shell(
            "twoObj",
            "Two Content",
            &format!(
                "{}{}{}",
                placeholder(2, "Title", "title", None, 457_200, 274_638, 8_229_600, 1_143_000),
                placeholder(3, "Content Left", "body", Some(1), 457_200, 1_600_200, 4_025_400, 4_525_963),
                placeholder(4, "Content Right", "body", Some(2), 4_661_400, 1_600_200, 4_025_400, 4_525_963),
            ),
        ),
        4 => layout_shell(
            "secHead",
            "Section Header",
            &format!(
                "{}{}",
                placeholder(2, "Title", "title", None, 457_200, 1_600_200, 8_229_600, 1_828_800),
                placeholder(3, "Subtitle", "body", Some(1), 457_200, 3_657_600, 8_229_600, 1_371_600),
            ),
        ),
        5 => layout_shell("blank", "Blank", ""),
        6 => layout_shell(
            "titleOnly",
            "Title Only",
            &placeholder(2, "Title", "title", None, 457_200, 274_638, 8_229_600, 1_143_000),
        ),
        7 => layout_shell(
            "obj",
            "Title and Big Content",
            &format!(
                "{}{}",
                placeholder(2, "Title", "title", None, 457_200, 274_638, 8_229_600, 800_000),
                placeholder(3, "Content", "body", Some(1), 457_200, 1_200_000, 8_229_600, 5_000_000),
            ),
        ),
        _ => layout_shell("blank", "Blank", ""),
    }
}

pub fn create_layout_rels_xml() -> &'static str {
    super::package_cache::layout_rels_xml()
}

pub fn create_master_rels_xml(layout_count: usize) -> String {
    let mut xml = String::from(
        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#,
    );
    for i in 1..=layout_count {
        xml.push_str("\n<Relationship Id=\"rId");
        append_usize(&mut xml, i);
        xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout\" Target=\"../slideLayouts/slideLayout");
        append_usize(&mut xml, i);
        xml.push_str(".xml\"/>");
    }
    xml.push_str("\n<Relationship Id=\"rId");
    append_usize(&mut xml, layout_count + 1);
    xml.push_str("\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme\" Target=\"../theme/theme1.xml\"/>\n</Relationships>");
    xml
}

pub fn append_layout_content_type_overrides(xml: &mut String, layout_count: usize) {
    for i in 1..=layout_count {
        xml.push_str("\n<Override PartName=\"/ppt/slideLayouts/slideLayout");
        append_usize(xml, i);
        xml.push_str(".xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml\"/>");
    }
}

pub fn layout_rel_target(layout_number: usize) -> String {
    let mut target = String::from("../slideLayouts/slideLayout");
    append_usize(&mut target, layout_number);
    target.push_str(".xml");
    target
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_all_standard_layouts_have_placeholders_or_blank() {
        for n in 1..=STANDARD_LAYOUT_COUNT {
            let xml = create_slide_layout_xml(n, None);
            assert!(xml.contains("p:sldLayout"), "layout {n}");
            assert!(xml.contains("type="), "layout {n}");
        }
        assert!(create_slide_layout_xml(2, None).contains(r#"type="body""#));
        assert!(create_slide_layout_xml(4, None).contains("secHead"));
    }

    #[test]
    fn test_master_rels_lists_all_layouts_before_theme() {
        let rels = create_master_rels_xml(7);
        assert!(rels.contains("slideLayout7.xml"));
        assert!(rels.contains(r#"Id="rId8""#));
        assert!(rels.contains("theme/theme1.xml"));
    }
}