pptx 0.1.0

A Rust library for creating and manipulating PowerPoint (.pptx) files
Documentation
//! Slide XML generation and manipulation functions.

use crate::error::{PptxError, PptxResult};
use crate::units::SlideId;

/// Build a minimal new slide XML blob.
///
/// This produces a valid `<p:sld>` element with an empty shape tree.
#[must_use]
pub fn new_slide_xml() -> Vec<u8> {
    let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld 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"><p:cSld><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></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>"#;
    xml.as_bytes().to_vec()
}

/// Build a minimal new notes slide XML blob.
#[must_use]
pub fn new_notes_slide_xml() -> Vec<u8> {
    let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:notes 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"><p:cSld><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><p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg"/></p:nvPr></p:nvSpPr><p:spPr/></p:sp><p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>"#;
    xml.as_bytes().to_vec()
}

/// Build a minimal notes master XML blob.
#[must_use]
pub fn new_notes_master_xml() -> Vec<u8> {
    let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:notesMaster 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"><p:cSld><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></p:spTree></p:cSld><p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/></p:notesMaster>"#;
    xml.as_bytes().to_vec()
}

/// Add a `<p:sldId>` entry to the presentation XML's `<p:sldIdLst>`.
///
/// If no `<p:sldIdLst>` exists, one is created. Returns the updated XML.
pub fn add_slide_id_to_presentation_xml(
    presentation_xml: &[u8],
    r_id: impl AsRef<str>,
    slide_id: SlideId,
) -> PptxResult<Vec<u8>> {
    let xml_str = std::str::from_utf8(presentation_xml)?;
    let r_id = r_id.as_ref();

    let new_entry = format!(r#"<p:sldId id="{slide_id}" r:id="{r_id}"/>"#);

    // Try to insert before the closing </p:sldIdLst> tag
    if let Some(pos) = xml_str.find("</p:sldIdLst>") {
        let mut result = Vec::with_capacity(presentation_xml.len() + new_entry.len());
        result.extend_from_slice(&presentation_xml[..pos]);
        result.extend_from_slice(new_entry.as_bytes());
        result.extend_from_slice(&presentation_xml[pos..]);
        return Ok(result);
    }

    // If there's an empty <p:sldIdLst/>, replace it
    if let Some(pos) = xml_str.find("<p:sldIdLst/>") {
        let mut result = Vec::with_capacity(presentation_xml.len() + new_entry.len() + 30);
        result.extend_from_slice(&presentation_xml[..pos]);
        result.extend_from_slice(b"<p:sldIdLst>");
        result.extend_from_slice(new_entry.as_bytes());
        result.extend_from_slice(b"</p:sldIdLst>");
        result.extend_from_slice(&presentation_xml[pos + "<p:sldIdLst/>".len()..]);
        return Ok(result);
    }

    // If there's no sldIdLst at all, insert one after the opening sldMasterIdLst block.
    // Look for </p:sldMasterIdLst> and insert after it.
    if let Some(pos) = xml_str.find("</p:sldMasterIdLst>") {
        let insert_pos = pos + "</p:sldMasterIdLst>".len();
        let mut result = Vec::with_capacity(presentation_xml.len() + new_entry.len() + 30);
        result.extend_from_slice(&presentation_xml[..insert_pos]);
        result.extend_from_slice(b"<p:sldIdLst>");
        result.extend_from_slice(new_entry.as_bytes());
        result.extend_from_slice(b"</p:sldIdLst>");
        result.extend_from_slice(&presentation_xml[insert_pos..]);
        return Ok(result);
    }

    Err(PptxError::InvalidXml(
        "could not find insertion point for sldIdLst in presentation XML".to_string(),
    ))
}

/// Compute the next available slide ID from existing slide IDs.
/// Slide IDs in OOXML start at 256 and increment.
pub fn next_slide_id(existing_ids: &[(String, SlideId)]) -> SlideId {
    // 255 is the correct fallback: OOXML slide IDs start at 256, so when
    // no slides exist yet, max+1 = 256.
    let max = existing_ids.iter().map(|(_, id)| id.0).max().unwrap_or(255);
    SlideId(max + 1)
}

/// Remove a `<p:sldId>` entry from the presentation XML by its r:id.
///
/// Returns the updated XML. If the entry is not found, the XML is returned unchanged.
pub fn remove_slide_id_from_presentation_xml(
    presentation_xml: &[u8],
    r_id: impl AsRef<str>,
) -> PptxResult<Vec<u8>> {
    let xml_str = std::str::from_utf8(presentation_xml)?;
    let r_id = r_id.as_ref();

    // Find and remove the <p:sldId ... r:id="rIdN"/> entry
    // Match patterns like: <p:sldId id="256" r:id="rId7"/>
    let pattern = format!(r#"r:id="{r_id}""#);

    // Find the sldId element containing our r:id and remove it.
    let removed = xml_str.find(&pattern).and_then(|rid_pos| {
        let tag_start = xml_str[..rid_pos].rfind("<p:sldId")?;
        let rel_end = xml_str[rid_pos..].find("/>")?;
        let tag_end = rid_pos + rel_end + 2;
        let mut v = Vec::with_capacity(presentation_xml.len());
        v.extend_from_slice(&presentation_xml[..tag_start]);
        v.extend_from_slice(&presentation_xml[tag_end..]);
        Some(v)
    });
    let modified = removed.is_some();
    let result_bytes = removed.unwrap_or_else(|| presentation_xml.to_vec());

    // If sldIdLst is now empty, replace <p:sldIdLst></p:sldIdLst> with <p:sldIdLst/>
    if modified {
        let result_str = std::str::from_utf8(&result_bytes)?;
        if let Some(pos) = result_str.find("<p:sldIdLst></p:sldIdLst>") {
            let empty_tag = b"<p:sldIdLst/>";
            let old_tag = b"<p:sldIdLst></p:sldIdLst>";
            let mut v = Vec::with_capacity(result_bytes.len());
            v.extend_from_slice(&result_bytes[..pos]);
            v.extend_from_slice(empty_tag);
            v.extend_from_slice(&result_bytes[pos + old_tag.len()..]);
            return Ok(v);
        }
    }

    Ok(result_bytes)
}

/// Reorder slides in the presentation XML by moving a slide from one position to another.
///
/// `from_index` and `to_index` are 0-based indices within the `<p:sldIdLst>`.
/// Returns the updated XML bytes.
pub fn reorder_slide_in_presentation_xml(
    presentation_xml: &[u8],
    from_index: usize,
    to_index: usize,
) -> PptxResult<Vec<u8>> {
    let xml_str = std::str::from_utf8(presentation_xml)?;

    // Extract the sldIdLst content
    let lst_start_tag = "<p:sldIdLst>";
    let lst_end_tag = "</p:sldIdLst>";

    let lst_start = xml_str.find(lst_start_tag).ok_or_else(|| {
        PptxError::InvalidXml("no sldIdLst found in presentation XML".to_string())
    })?;
    let lst_end = xml_str.find(lst_end_tag).ok_or_else(|| {
        PptxError::InvalidXml("no closing sldIdLst found in presentation XML".to_string())
    })?;

    let content_start = lst_start + lst_start_tag.len();
    let inner = &xml_str[content_start..lst_end];

    // Parse individual <p:sldId .../> entries
    let mut entries: Vec<&str> = Vec::new();
    let mut search_start = 0;
    while let Some(tag_start) = inner[search_start..].find("<p:sldId") {
        let abs_start = search_start + tag_start;
        let rest = &inner[abs_start..];
        if let Some(tag_end) = rest.find("/>") {
            let entry = &inner[abs_start..abs_start + tag_end + 2];
            entries.push(entry);
            search_start = abs_start + tag_end + 2;
        } else {
            break;
        }
    }

    if from_index >= entries.len() || to_index >= entries.len() {
        return Err(PptxError::InvalidXml(format!(
            "slide reorder indices out of range: from={}, to={}, count={}",
            from_index,
            to_index,
            entries.len()
        )));
    }

    // Reorder
    let entry = entries.remove(from_index);
    entries.insert(to_index, entry);

    // Rebuild directly into Vec<u8>
    let new_inner_len: usize = entries.iter().map(|e| e.len()).sum();
    let mut result = Vec::with_capacity(presentation_xml.len() + new_inner_len);
    result.extend_from_slice(&presentation_xml[..content_start]);
    for e in &entries {
        result.extend_from_slice(e.as_bytes());
    }
    result.extend_from_slice(&presentation_xml[lst_end..]);

    Ok(result)
}

/// Update the `<p:sldSz>` element in presentation XML with new cx/cy values.
///
/// Returns the updated XML bytes.
pub fn set_slide_size_in_xml(presentation_xml: &[u8], cx: i64, cy: i64) -> PptxResult<Vec<u8>> {
    let xml_str = std::str::from_utf8(presentation_xml)?;

    // Find <p:sldSz ... /> or <p:sldSz ...>
    // We need to replace the cx and cy attribute values
    if let Some(sld_sz_pos) = xml_str.find("<p:sldSz") {
        let after = &xml_str[sld_sz_pos..];
        // Find end of the sldSz element
        let end_pos = after
            .find("/>")
            .or_else(|| after.find('>'))
            .ok_or_else(|| PptxError::InvalidXml("malformed sldSz element".to_string()))?;
        let elem_str = &after[..end_pos
            + if after.as_bytes().get(end_pos + 1) == Some(&b'>') {
                2
            } else {
                1
            }];

        // Build new element preserving the type attribute if present
        let type_attr = elem_str.find("type=\"").and_then(|type_pos| {
            let rest = &elem_str[type_pos + 6..];
            rest.find('"').map(|end| &rest[..end])
        });

        let new_elem = type_attr.map_or_else(
            || format!(r#"<p:sldSz cx="{cx}" cy="{cy}"/>"#),
            |t| format!(r#"<p:sldSz cx="{cx}" cy="{cy}" type="{t}"/>"#),
        );

        // Find exact end position of the sldSz element
        let abs_end = sld_sz_pos + end_pos;
        let abs_end = if presentation_xml.get(abs_end) == Some(&b'/') {
            abs_end + 2 // skip />
        } else {
            abs_end + 1 // skip >
        };

        let mut result = Vec::with_capacity(presentation_xml.len() + new_elem.len());
        result.extend_from_slice(&presentation_xml[..sld_sz_pos]);
        result.extend_from_slice(new_elem.as_bytes());
        result.extend_from_slice(&presentation_xml[abs_end..]);
        Ok(result)
    } else {
        Ok(presentation_xml.to_vec())
    }
}