office2pdf 0.5.0

Convert DOCX, XLSX, and PPTX files to PDF using pure Rust
Documentation
use super::*;

fn make_smartart_data_xml(items: &[&str]) -> String {
    let mut xml = String::from(
        r#"<?xml version="1.0" encoding="UTF-8"?><dgm:dataModel xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><dgm:ptLst>"#,
    );
    xml.push_str(
        r#"<dgm:pt modelId="0" type="doc"><dgm:prSet/><dgm:spPr/><dgm:t><a:bodyPr/><a:p><a:r><a:t>Root</a:t></a:r></a:p></dgm:t></dgm:pt>"#,
    );
    for (index, item) in items.iter().enumerate() {
        xml.push_str(&format!(
            r#"<dgm:pt modelId="{}" type="node"><dgm:prSet/><dgm:spPr/><dgm:t><a:bodyPr/><a:p><a:r><a:t>{item}</a:t></a:r></a:p></dgm:t></dgm:pt>"#,
            index + 1
        ));
    }
    xml.push_str("</dgm:ptLst>");
    xml.push_str("<dgm:cxnLst>");
    for (index, _) in items.iter().enumerate() {
        xml.push_str(&format!(
            r#"<dgm:cxn modelId="{}" type="parOf" srcId="0" destId="{}"/>"#,
            100 + index,
            index + 1,
        ));
    }
    xml.push_str("</dgm:cxnLst>");
    xml.push_str("</dgm:dataModel>");
    xml
}

fn make_smartart_graphic_frame(x: i64, y: i64, cx: i64, cy: i64, dm_rid: &str) -> String {
    format!(
        r#"<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="4" name="SmartArt"/><p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="{x}" y="{y}"/><a:ext cx="{cx}" cy="{cy}"/></p:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/diagram"><dgm:relIds xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" r:dm="{dm_rid}" r:lo="rId99" r:qs="rId98" r:cs="rId97"/></a:graphicData></a:graphic></p:graphicFrame>"#
    )
}

fn build_test_pptx_with_smartart(
    slide_cx_emu: i64,
    slide_cy_emu: i64,
    slide_xml: &str,
    data_rid: &str,
    data_xml: &str,
) -> Vec<u8> {
    let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
    let opts = FileOptions::default();

    zip.start_file("[Content_Types].xml", opts).unwrap();
    zip.write_all(
        br#"<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/></Types>"#,
    )
    .unwrap();

    zip.start_file("_rels/.rels", opts).unwrap();
    zip.write_all(
        br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/></Relationships>"#,
    )
    .unwrap();

    let presentation_xml = format!(
        r#"<?xml version="1.0" encoding="UTF-8"?><p:presentation 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:sldSz cx="{slide_cx_emu}" cy="{slide_cy_emu}"/><p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst></p:presentation>"#
    );
    zip.start_file("ppt/presentation.xml", opts).unwrap();
    zip.write_all(presentation_xml.as_bytes()).unwrap();

    zip.start_file("ppt/_rels/presentation.xml.rels", opts)
        .unwrap();
    zip.write_all(
        br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>"#,
    )
    .unwrap();

    zip.start_file("ppt/slides/slide1.xml", opts).unwrap();
    zip.write_all(slide_xml.as_bytes()).unwrap();

    let slide_rels = format!(
        r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="{data_rid}" Type="http://schemas.microsoft.com/office/2007/relationships/diagramData" Target="../diagrams/data1.xml"/></Relationships>"#
    );
    zip.start_file("ppt/slides/_rels/slide1.xml.rels", opts)
        .unwrap();
    zip.write_all(slide_rels.as_bytes()).unwrap();

    zip.start_file("ppt/diagrams/data1.xml", opts).unwrap();
    zip.write_all(data_xml.as_bytes()).unwrap();

    zip.finish().unwrap().into_inner()
}

fn get_smartart(elem: &FixedElement) -> &SmartArt {
    match &elem.kind {
        FixedElementKind::SmartArt(smartart) => smartart,
        _ => panic!("Expected SmartArt, got {:?}", elem.kind),
    }
}

#[test]
fn test_slide_with_smartart_produces_items() {
    let smartart_frame =
        make_smartart_graphic_frame(914_400, 1_828_800, 5_486_400, 3_086_100, "rId5");
    let slide_xml = make_slide_xml(&[smartart_frame]);
    let data_xml = make_smartart_data_xml(&["Step 1", "Step 2", "Step 3"]);
    let data = build_test_pptx_with_smartart(SLIDE_CX, SLIDE_CY, &slide_xml, "rId5", &data_xml);

    let parser = PptxParser;
    let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();

    let page = first_fixed_page(&doc);
    let smartart_elements: Vec<_> = page
        .elements
        .iter()
        .filter(|element| matches!(element.kind, FixedElementKind::SmartArt(_)))
        .collect();
    assert_eq!(smartart_elements.len(), 1);

    let smartart = get_smartart(smartart_elements[0]);
    let texts: Vec<&str> = smartart
        .items
        .iter()
        .map(|item| item.text.as_str())
        .collect();
    assert_eq!(texts, vec!["Step 1", "Step 2", "Step 3"]);
    assert!(smartart.items.iter().all(|item| item.depth == 0));
    assert!((smartart_elements[0].x - 72.0).abs() < 0.1);
    assert!((smartart_elements[0].y - 144.0).abs() < 0.1);
}

#[test]
fn test_slide_with_smartart_and_text_box() {
    let text_box = make_text_box(100_000, 100_000, 500_000, 200_000, "Title");
    let smartart_frame =
        make_smartart_graphic_frame(500_000, 500_000, 3_000_000, 2_000_000, "rId5");
    let slide_xml = make_slide_xml(&[text_box, smartart_frame]);
    let data_xml = make_smartart_data_xml(&["Item A", "Item B"]);
    let data = build_test_pptx_with_smartart(SLIDE_CX, SLIDE_CY, &slide_xml, "rId5", &data_xml);

    let parser = PptxParser;
    let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();

    let page = first_fixed_page(&doc);
    let smartart_count = page
        .elements
        .iter()
        .filter(|element| matches!(element.kind, FixedElementKind::SmartArt(_)))
        .count();
    let text_box_count = page
        .elements
        .iter()
        .filter(|element| matches!(element.kind, FixedElementKind::TextBox(_)))
        .count();
    assert_eq!(smartart_count, 1);
    assert!(text_box_count >= 1);

    let smartart_element = page
        .elements
        .iter()
        .find(|element| matches!(element.kind, FixedElementKind::SmartArt(_)))
        .unwrap();
    let smartart = get_smartart(smartart_element);
    let texts: Vec<&str> = smartart
        .items
        .iter()
        .map(|item| item.text.as_str())
        .collect();
    assert_eq!(texts, vec!["Item A", "Item B"]);
}

#[test]
fn test_slide_without_smartart_no_smartart_elements() {
    let text_box = make_text_box(0, 0, 500_000, 200_000, "No SmartArt");
    let slide_xml = make_slide_xml(&[text_box]);
    let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide_xml]);

    let parser = PptxParser;
    let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();

    let page = first_fixed_page(&doc);
    let smartart_count = page
        .elements
        .iter()
        .filter(|element| matches!(element.kind, FixedElementKind::SmartArt(_)))
        .count();
    assert_eq!(smartart_count, 0);
}