office2pdf 0.5.0

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

fn make_chart_graphic_frame(x: i64, y: i64, cx: i64, cy: i64, chart_rid: &str) -> String {
    format!(
        r#"<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="5" name="Chart"/><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/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" r:id="{chart_rid}"/></a:graphicData></a:graphic></p:graphicFrame>"#
    )
}

fn make_bar_chart_xml(title: &str, categories: &[&str], values: &[f64]) -> String {
    let mut category_xml = String::new();
    for (index, category) in categories.iter().enumerate() {
        category_xml.push_str(&format!(
            r#"<c:pt idx="{index}"><c:v>{category}</c:v></c:pt>"#
        ));
    }
    let mut value_xml = String::new();
    for (index, value) in values.iter().enumerate() {
        value_xml.push_str(&format!(r#"<c:pt idx="{index}"><c:v>{value}</c:v></c:pt>"#));
    }
    format!(
        r#"<?xml version="1.0" encoding="UTF-8"?><c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:chart><c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>{title}</a:t></a:r></a:p></c:rich></c:tx></c:title><c:plotArea><c:barChart><c:ser><c:tx><c:strRef><c:strCache><c:pt idx="0"><c:v>Series 1</c:v></c:pt></c:strCache></c:strRef></c:tx><c:cat><c:strRef><c:strCache>{category_xml}</c:strCache></c:strRef></c:cat><c:val><c:numRef><c:numCache>{value_xml}</c:numCache></c:numRef></c:val></c:ser></c:barChart></c:plotArea></c:chart></c:chartSpace>"#
    )
}

fn build_test_pptx_with_chart(
    slide_cx_emu: i64,
    slide_cy_emu: i64,
    slide_xml: &str,
    chart_rid: &str,
    chart_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="{chart_rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" Target="../charts/chart1.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/charts/chart1.xml", opts).unwrap();
    zip.write_all(chart_xml.as_bytes()).unwrap();

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

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

#[test]
fn test_slide_with_chart_produces_chart_element() {
    let chart_frame = make_chart_graphic_frame(914_400, 1_828_800, 5_486_400, 3_086_100, "rId5");
    let slide_xml = make_slide_xml(&[chart_frame]);
    let chart_xml = make_bar_chart_xml("Sales Data", &["Q1", "Q2", "Q3"], &[100.0, 200.0, 150.0]);
    let data = build_test_pptx_with_chart(SLIDE_CX, SLIDE_CY, &slide_xml, "rId5", &chart_xml);

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

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

    let chart = get_chart(chart_elements[0]);
    assert_eq!(chart.title.as_deref(), Some("Sales Data"));
    assert_eq!(chart.categories, vec!["Q1", "Q2", "Q3"]);
    assert_eq!(chart.series.len(), 1);
    assert_eq!(chart.series[0].values, vec![100.0, 200.0, 150.0]);
    assert!((chart_elements[0].x - 72.0).abs() < 0.1);
    assert!((chart_elements[0].y - 144.0).abs() < 0.1);
}

#[test]
fn test_slide_with_chart_and_text_box() {
    let text_box = make_text_box(100_000, 100_000, 500_000, 200_000, "Title");
    let chart_frame = make_chart_graphic_frame(500_000, 500_000, 3_000_000, 2_000_000, "rId5");
    let slide_xml = make_slide_xml(&[text_box, chart_frame]);
    let chart_xml = make_bar_chart_xml("Revenue", &["Jan", "Feb"], &[50.0, 75.0]);
    let data = build_test_pptx_with_chart(SLIDE_CX, SLIDE_CY, &slide_xml, "rId5", &chart_xml);

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

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

#[test]
fn test_slide_without_chart_no_chart_elements() {
    let text_box = make_text_box(0, 0, 500_000, 200_000, "No Chart");
    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 chart_count = page
        .elements
        .iter()
        .filter(|element| matches!(element.kind, FixedElementKind::Chart(_)))
        .count();
    assert_eq!(chart_count, 0);
}

#[test]
fn test_scan_chart_refs_basic() {
    let slide_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
        <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"
               xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart">
          <p:cSld><p:spTree>
            <p:graphicFrame>
              <p:nvGraphicFramePr>
                <p:cNvPr id="5" name="Chart"/>
                <p:cNvGraphicFramePr/>
                <p:nvPr/>
              </p:nvGraphicFramePr>
              <p:xfrm>
                <a:off x="914400" y="1828800"/>
                <a:ext cx="5486400" cy="3086100"/>
              </p:xfrm>
              <a:graphic>
                <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">
                  <c:chart r:id="rId5"/>
                </a:graphicData>
              </a:graphic>
            </p:graphicFrame>
          </p:spTree></p:cSld>
        </p:sld>"#;

    let refs = scan_chart_refs(slide_xml);
    assert_eq!(refs.len(), 1);
    assert_eq!(refs[0].x, 914400);
    assert_eq!(refs[0].y, 1828800);
    assert_eq!(refs[0].cx, 5486400);
    assert_eq!(refs[0].cy, 3086100);
    assert_eq!(refs[0].chart_rid, "rId5");
}

#[test]
fn test_scan_chart_refs_no_chart() {
    let slide_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
        <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:sp>
              <p:nvSpPr><p:cNvPr id="2" name="TextBox"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
              <p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="100"/></a:xfrm></p:spPr>
              <p:txBody><a:bodyPr/><a:p><a:r><a:t>Hello</a:t></a:r></a:p></p:txBody>
            </p:sp>
          </p:spTree></p:cSld>
        </p:sld>"#;

    let refs = scan_chart_refs(slide_xml);
    assert!(refs.is_empty());
}