use std::io::{Seek, Write};
use std::path::Path;
use quick_xml::Writer;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use crate::core::opc::{OpcWriter, PartName};
use crate::core::relationships::rel_types;
use super::Result;
const CT_PRESENTATION: &str =
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml";
const CT_SLIDE: &str = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
const CT_SLIDE_LAYOUT: &str =
"application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml";
const CT_SLIDE_MASTER: &str =
"application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml";
use crate::core::xml::ns::{DRAWING_ML_STR as NS_DML, PML_STR as NS_PML, R_STR as NS_REL};
const SLIDE_WIDTH: &str = "12192000";
const SLIDE_HEIGHT: &str = "6858000";
#[derive(Debug, Clone, Default)]
pub struct Run {
pub text: String,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub color: Option<String>,
pub font_size_pt: Option<f64>,
pub font_name: Option<String>,
}
impl Run {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
pub fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
pub fn color(mut self, hex: impl Into<String>) -> Self {
self.color = Some(hex.into());
self
}
pub fn font_size(mut self, pt: f64) -> Self {
self.font_size_pt = Some(pt);
self
}
pub fn font(mut self, name: impl Into<String>) -> Self {
self.font_name = Some(name.into());
self
}
fn has_rpr(&self) -> bool {
self.bold
|| self.italic
|| self.underline
|| self.strikethrough
|| self.color.is_some()
|| self.font_size_pt.is_some()
|| self.font_name.is_some()
}
}
impl From<&str> for Run {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for Run {
fn from(s: String) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone)]
enum BodyItem {
Text(String),
RichText(Vec<Run>),
BulletList(Vec<String>),
TextBox(Vec<Run>, i64, i64, i64, i64),
}
#[derive(Debug, Clone)]
pub struct SlideData {
pub title: Option<String>,
body_items: Vec<BodyItem>,
}
impl SlideData {
fn new() -> Self {
Self {
title: None,
body_items: Vec::new(),
}
}
pub fn set_title(&mut self, title: &str) -> &mut Self {
self.title = Some(title.to_string());
self
}
pub fn add_text(&mut self, text: &str) -> &mut Self {
self.body_items.push(BodyItem::Text(text.to_string()));
self
}
pub fn add_rich_text(&mut self, runs: &[Run]) -> &mut Self {
self.body_items.push(BodyItem::RichText(runs.to_vec()));
self
}
pub fn add_bullet_list(&mut self, items: &[&str]) -> &mut Self {
let owned: Vec<String> = items.iter().map(|s| s.to_string()).collect();
self.body_items.push(BodyItem::BulletList(owned));
self
}
pub fn add_text_box(&mut self, text: &str, x: i64, y: i64, cx: i64, cy: i64) -> &mut Self {
self.body_items
.push(BodyItem::TextBox(vec![Run::new(text)], x, y, cx, cy));
self
}
pub fn add_rich_text_box(
&mut self,
runs: &[Run],
x: i64,
y: i64,
cx: i64,
cy: i64,
) -> &mut Self {
self.body_items
.push(BodyItem::TextBox(runs.to_vec(), x, y, cx, cy));
self
}
fn has_placeholder_body(&self) -> bool {
self.body_items
.iter()
.any(|i| !matches!(i, BodyItem::TextBox(..)))
}
}
pub struct PptxWriter {
slides: Vec<SlideData>,
}
impl PptxWriter {
pub fn new() -> Self {
Self { slides: Vec::new() }
}
pub fn add_slide(&mut self) -> &mut SlideData {
self.slides.push(SlideData::new());
self.slides.last_mut().expect("just pushed")
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let opc = OpcWriter::create(path)?;
self.write_opc(opc)?;
Ok(())
}
pub fn write_to<W: Write + Seek>(&self, writer: W) -> Result<()> {
let opc = OpcWriter::new(writer)?;
self.write_opc(opc)?;
Ok(())
}
fn write_opc<W: Write + Seek>(&self, mut opc: OpcWriter<W>) -> Result<()> {
let pres_part = PartName::new("/ppt/presentation.xml")?;
let master_part = PartName::new("/ppt/slideMasters/slideMaster1.xml")?;
let layout_part = PartName::new("/ppt/slideLayouts/slideLayout1.xml")?;
opc.add_package_rel(rel_types::OFFICE_DOCUMENT, "ppt/presentation.xml");
opc.add_part_rel(&pres_part, rel_types::SLIDE_MASTER, "slideMasters/slideMaster1.xml");
let mut slide_parts = Vec::with_capacity(self.slides.len());
for i in 0..self.slides.len() {
let idx = i + 1;
let slide_part = PartName::new(&format!("/ppt/slides/slide{idx}.xml"))?;
opc.add_part_rel(&pres_part, rel_types::SLIDE, &format!("slides/slide{idx}.xml"));
slide_parts.push(slide_part);
}
opc.add_part_rel(&master_part, rel_types::SLIDE_LAYOUT, "../slideLayouts/slideLayout1.xml");
for slide_part in &slide_parts {
opc.add_part_rel(
slide_part,
rel_types::SLIDE_LAYOUT,
"../slideLayouts/slideLayout1.xml",
);
}
let pres_xml = generate_presentation_xml(self.slides.len());
opc.add_part(&pres_part, CT_PRESENTATION, &pres_xml)?;
let master_xml = generate_slide_master_xml();
opc.add_part(&master_part, CT_SLIDE_MASTER, &master_xml)?;
let layout_xml = generate_slide_layout_xml();
opc.add_part(&layout_part, CT_SLIDE_LAYOUT, &layout_xml)?;
for (i, slide) in self.slides.iter().enumerate() {
let slide_xml = generate_slide_xml(slide);
opc.add_part(&slide_parts[i], CT_SLIDE, &slide_xml)?;
}
opc.finish()?;
Ok(())
}
}
impl Default for PptxWriter {
fn default() -> Self {
Self::new()
}
}
fn write_decl(w: &mut Writer<Vec<u8>>) {
w.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), Some("yes"))))
.expect("write decl");
}
fn write_text_element(w: &mut Writer<Vec<u8>>, tag: &str, text: &str) {
w.write_event(Event::Start(BytesStart::new(tag)))
.expect("write start");
w.write_event(Event::Text(BytesText::new(text)))
.expect("write text");
w.write_event(Event::End(BytesEnd::new(tag)))
.expect("write end");
}
fn write_empty(w: &mut Writer<Vec<u8>>, tag: &str) {
w.write_event(Event::Empty(BytesStart::new(tag)))
.expect("write empty");
}
fn pml_root(tag: &str) -> BytesStart<'_> {
let mut elem = BytesStart::new(tag);
elem.push_attribute(("xmlns:p", NS_PML));
elem.push_attribute(("xmlns:a", NS_DML));
elem.push_attribute(("xmlns:r", NS_REL));
elem
}
fn write_nv_grp_sp_pr(w: &mut Writer<Vec<u8>>) {
w.write_event(Event::Start(BytesStart::new("p:nvGrpSpPr")))
.expect("write");
let mut cnv_pr = BytesStart::new("p:cNvPr");
cnv_pr.push_attribute(("id", "1"));
cnv_pr.push_attribute(("name", ""));
w.write_event(Event::Empty(cnv_pr)).expect("write");
write_empty(w, "p:cNvGrpSpPr");
write_empty(w, "p:nvPr");
w.write_event(Event::End(BytesEnd::new("p:nvGrpSpPr")))
.expect("write");
}
fn write_dml_run(w: &mut Writer<Vec<u8>>, run: &Run) {
w.write_event(Event::Start(BytesStart::new("a:r")))
.expect("write");
if run.has_rpr() {
let mut rpr = BytesStart::new("a:rPr");
rpr.push_attribute(("lang", "en-US"));
rpr.push_attribute(("dirty", "0"));
if run.bold {
rpr.push_attribute(("b", "1"));
}
if run.italic {
rpr.push_attribute(("i", "1"));
}
if run.underline {
rpr.push_attribute(("u", "sng"));
}
if run.strikethrough {
rpr.push_attribute(("strike", "sngStrike"));
}
if let Some(pt) = run.font_size_pt {
let hundredths = (pt * 100.0).round() as u32;
rpr.push_attribute(("sz", hundredths.to_string().as_str()));
}
if run.color.is_some() || run.font_name.is_some() {
w.write_event(Event::Start(rpr)).expect("write rPr start");
if let Some(ref hex) = run.color {
w.write_event(Event::Start(BytesStart::new("a:solidFill")))
.expect("write");
let mut clr = BytesStart::new("a:srgbClr");
clr.push_attribute(("val", hex.as_str()));
w.write_event(Event::Empty(clr)).expect("write");
w.write_event(Event::End(BytesEnd::new("a:solidFill")))
.expect("write");
}
if let Some(ref name) = run.font_name {
let mut latin = BytesStart::new("a:latin");
latin.push_attribute(("typeface", name.as_str()));
w.write_event(Event::Empty(latin)).expect("write");
}
w.write_event(Event::End(BytesEnd::new("a:rPr")))
.expect("write rPr end");
} else {
w.write_event(Event::Empty(rpr)).expect("write rPr empty");
}
}
write_text_element(w, "a:t", &run.text);
w.write_event(Event::End(BytesEnd::new("a:r")))
.expect("write");
}
fn generate_presentation_xml(slide_count: usize) -> Vec<u8> {
let mut w = Writer::new(Vec::new());
write_decl(&mut w);
w.write_event(Event::Start(pml_root("p:presentation")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:sldMasterIdLst")))
.expect("write");
let mut master_id = BytesStart::new("p:sldMasterId");
master_id.push_attribute(("id", "2147483648"));
master_id.push_attribute(("r:id", "rId1"));
w.write_event(Event::Empty(master_id)).expect("write");
w.write_event(Event::End(BytesEnd::new("p:sldMasterIdLst")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:sldIdLst")))
.expect("write");
for i in 0..slide_count {
let slide_id_val = 256 + i as u32;
let r_id = format!("rId{}", i + 2);
let mut slide_id = BytesStart::new("p:sldId");
slide_id.push_attribute(("id", slide_id_val.to_string().as_str()));
slide_id.push_attribute(("r:id", r_id.as_str()));
w.write_event(Event::Empty(slide_id)).expect("write");
}
w.write_event(Event::End(BytesEnd::new("p:sldIdLst")))
.expect("write");
let mut sld_sz = BytesStart::new("p:sldSz");
sld_sz.push_attribute(("cx", SLIDE_WIDTH));
sld_sz.push_attribute(("cy", SLIDE_HEIGHT));
w.write_event(Event::Empty(sld_sz)).expect("write");
w.write_event(Event::End(BytesEnd::new("p:presentation")))
.expect("write");
w.into_inner()
}
fn generate_slide_master_xml() -> Vec<u8> {
let mut w = Writer::new(Vec::new());
write_decl(&mut w);
w.write_event(Event::Start(pml_root("p:sldMaster")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:cSld")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:spTree")))
.expect("write");
write_nv_grp_sp_pr(&mut w);
write_empty(&mut w, "p:grpSpPr");
w.write_event(Event::End(BytesEnd::new("p:spTree")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:cSld")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:sldLayoutIdLst")))
.expect("write");
let mut layout_id = BytesStart::new("p:sldLayoutId");
layout_id.push_attribute(("id", "2147483649"));
layout_id.push_attribute(("r:id", "rId1"));
w.write_event(Event::Empty(layout_id)).expect("write");
w.write_event(Event::End(BytesEnd::new("p:sldLayoutIdLst")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:sldMaster")))
.expect("write");
w.into_inner()
}
fn generate_slide_layout_xml() -> Vec<u8> {
let mut w = Writer::new(Vec::new());
write_decl(&mut w);
let mut root = pml_root("p:sldLayout");
root.push_attribute(("type", "blank"));
w.write_event(Event::Start(root)).expect("write");
w.write_event(Event::Start(BytesStart::new("p:cSld")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:spTree")))
.expect("write");
write_nv_grp_sp_pr(&mut w);
write_empty(&mut w, "p:grpSpPr");
w.write_event(Event::End(BytesEnd::new("p:spTree")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:cSld")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:sldLayout")))
.expect("write");
w.into_inner()
}
fn generate_slide_xml(slide: &SlideData) -> Vec<u8> {
let mut w = Writer::new(Vec::new());
write_decl(&mut w);
w.write_event(Event::Start(pml_root("p:sld")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:cSld")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:spTree")))
.expect("write");
write_nv_grp_sp_pr(&mut w);
write_empty(&mut w, "p:grpSpPr");
let mut next_id: u32 = 2;
if let Some(ref title) = slide.title {
write_title_shape(&mut w, next_id, title);
next_id += 1;
}
if slide.has_placeholder_body() {
let placeholder_items: Vec<&BodyItem> = slide
.body_items
.iter()
.filter(|i| !matches!(i, BodyItem::TextBox(..)))
.collect();
write_body_shape(&mut w, next_id, &placeholder_items);
next_id += 1;
}
for item in &slide.body_items {
if let BodyItem::TextBox(runs, x, y, cx, cy) = item {
write_text_box_shape(&mut w, next_id, runs, *x, *y, *cx, *cy);
next_id += 1;
}
}
w.write_event(Event::End(BytesEnd::new("p:spTree")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:cSld")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:sld")))
.expect("write");
w.into_inner()
}
fn write_title_shape(w: &mut Writer<Vec<u8>>, id: u32, title: &str) {
let id_str = id.to_string();
w.write_event(Event::Start(BytesStart::new("p:sp")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:nvSpPr")))
.expect("write");
let mut cnv_pr = BytesStart::new("p:cNvPr");
cnv_pr.push_attribute(("id", id_str.as_str()));
cnv_pr.push_attribute(("name", "Title 1"));
w.write_event(Event::Empty(cnv_pr)).expect("write");
w.write_event(Event::Start(BytesStart::new("p:cNvSpPr")))
.expect("write");
let mut locks = BytesStart::new("a:spLocks");
locks.push_attribute(("noGrp", "1"));
w.write_event(Event::Empty(locks)).expect("write");
w.write_event(Event::End(BytesEnd::new("p:cNvSpPr")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:nvPr")))
.expect("write");
let mut ph = BytesStart::new("p:ph");
ph.push_attribute(("type", "title"));
w.write_event(Event::Empty(ph)).expect("write");
w.write_event(Event::End(BytesEnd::new("p:nvPr")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:nvSpPr")))
.expect("write");
write_empty(w, "p:spPr");
w.write_event(Event::Start(BytesStart::new("p:txBody")))
.expect("write");
write_empty(w, "a:bodyPr");
write_plain_paragraph(w, title);
w.write_event(Event::End(BytesEnd::new("p:txBody")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:sp")))
.expect("write");
}
fn write_body_shape(w: &mut Writer<Vec<u8>>, id: u32, items: &[&BodyItem]) {
let id_str = id.to_string();
w.write_event(Event::Start(BytesStart::new("p:sp")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:nvSpPr")))
.expect("write");
let mut cnv_pr = BytesStart::new("p:cNvPr");
cnv_pr.push_attribute(("id", id_str.as_str()));
cnv_pr.push_attribute(("name", "Body 2"));
w.write_event(Event::Empty(cnv_pr)).expect("write");
w.write_event(Event::Start(BytesStart::new("p:cNvSpPr")))
.expect("write");
let mut locks = BytesStart::new("a:spLocks");
locks.push_attribute(("noGrp", "1"));
w.write_event(Event::Empty(locks)).expect("write");
w.write_event(Event::End(BytesEnd::new("p:cNvSpPr")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:nvPr")))
.expect("write");
let mut ph = BytesStart::new("p:ph");
ph.push_attribute(("type", "body"));
ph.push_attribute(("idx", "1"));
w.write_event(Event::Empty(ph)).expect("write");
w.write_event(Event::End(BytesEnd::new("p:nvPr")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:nvSpPr")))
.expect("write");
write_empty(w, "p:spPr");
w.write_event(Event::Start(BytesStart::new("p:txBody")))
.expect("write");
write_empty(w, "a:bodyPr");
for item in items {
match item {
BodyItem::Text(text) => write_plain_paragraph(w, text),
BodyItem::RichText(runs) => write_rich_paragraph(w, runs),
BodyItem::BulletList(bullets) => {
for bullet in bullets {
write_bullet_paragraph(w, bullet);
}
},
BodyItem::TextBox(..) => {}, }
}
w.write_event(Event::End(BytesEnd::new("p:txBody")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:sp")))
.expect("write");
}
fn write_text_box_shape(
w: &mut Writer<Vec<u8>>,
id: u32,
runs: &[Run],
x: i64,
y: i64,
cx: i64,
cy: i64,
) {
let id_str = id.to_string();
let name = format!("TextBox {id}");
w.write_event(Event::Start(BytesStart::new("p:sp")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:nvSpPr")))
.expect("write");
let mut cnv_pr = BytesStart::new("p:cNvPr");
cnv_pr.push_attribute(("id", id_str.as_str()));
cnv_pr.push_attribute(("name", name.as_str()));
w.write_event(Event::Empty(cnv_pr)).expect("write");
let mut cnv_sp_pr = BytesStart::new("p:cNvSpPr");
cnv_sp_pr.push_attribute(("txBox", "1"));
w.write_event(Event::Empty(cnv_sp_pr)).expect("write");
write_empty(w, "p:nvPr");
w.write_event(Event::End(BytesEnd::new("p:nvSpPr")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:spPr")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("a:xfrm")))
.expect("write");
let mut off = BytesStart::new("a:off");
off.push_attribute(("x", x.to_string().as_str()));
off.push_attribute(("y", y.to_string().as_str()));
w.write_event(Event::Empty(off)).expect("write");
let mut ext = BytesStart::new("a:ext");
ext.push_attribute(("cx", cx.to_string().as_str()));
ext.push_attribute(("cy", cy.to_string().as_str()));
w.write_event(Event::Empty(ext)).expect("write");
w.write_event(Event::End(BytesEnd::new("a:xfrm")))
.expect("write");
let mut geom = BytesStart::new("a:prstGeom");
geom.push_attribute(("prst", "rect"));
w.write_event(Event::Start(geom)).expect("write");
write_empty(w, "a:avLst");
w.write_event(Event::End(BytesEnd::new("a:prstGeom")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:spPr")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("p:txBody")))
.expect("write");
let mut body_pr = BytesStart::new("a:bodyPr");
body_pr.push_attribute(("wrap", "square"));
w.write_event(Event::Empty(body_pr)).expect("write");
write_rich_paragraph(w, runs);
w.write_event(Event::End(BytesEnd::new("p:txBody")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("p:sp")))
.expect("write");
}
fn write_plain_paragraph(w: &mut Writer<Vec<u8>>, text: &str) {
w.write_event(Event::Start(BytesStart::new("a:p")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("a:r")))
.expect("write");
write_text_element(w, "a:t", text);
w.write_event(Event::End(BytesEnd::new("a:r")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("a:p")))
.expect("write");
}
fn write_rich_paragraph(w: &mut Writer<Vec<u8>>, runs: &[Run]) {
w.write_event(Event::Start(BytesStart::new("a:p")))
.expect("write");
for run in runs {
write_dml_run(w, run);
}
w.write_event(Event::End(BytesEnd::new("a:p")))
.expect("write");
}
fn write_bullet_paragraph(w: &mut Writer<Vec<u8>>, text: &str) {
w.write_event(Event::Start(BytesStart::new("a:p")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("a:pPr")))
.expect("write");
let mut bu = BytesStart::new("a:buChar");
bu.push_attribute(("char", "\u{2022}"));
w.write_event(Event::Empty(bu)).expect("write");
w.write_event(Event::End(BytesEnd::new("a:pPr")))
.expect("write");
w.write_event(Event::Start(BytesStart::new("a:r")))
.expect("write");
write_text_element(w, "a:t", text);
w.write_event(Event::End(BytesEnd::new("a:r")))
.expect("write");
w.write_event(Event::End(BytesEnd::new("a:p")))
.expect("write");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pptx::PptxDocument;
use std::io::Cursor;
fn roundtrip(writer: PptxWriter) -> PptxDocument {
let mut buf = Cursor::new(Vec::new());
writer.write_to(&mut buf).unwrap();
buf.set_position(0);
PptxDocument::from_reader(buf).unwrap()
}
#[test]
fn rich_runs_roundtrip() {
let mut writer = PptxWriter::new();
writer
.add_slide()
.set_title("Test")
.add_rich_text(&[Run::new("Bold").bold(), Run::new(" red").color("FF0000")]);
let doc = roundtrip(writer);
let text = doc.plain_text();
assert!(text.contains("Bold"));
assert!(text.contains("red"));
}
#[test]
fn text_box_roundtrip() {
let mut writer = PptxWriter::new();
writer
.add_slide()
.add_text_box("Floating note", 1_000_000, 5_000_000, 3_000_000, 500_000);
let doc = roundtrip(writer);
let text = doc.plain_text();
assert!(text.contains("Floating note"));
}
#[test]
fn rich_text_box_roundtrip() {
let mut writer = PptxWriter::new();
writer.add_slide().add_rich_text_box(
&[
Run::new("Big").font_size(24.0).bold(),
Run::new(" label").italic(),
],
500_000,
500_000,
4_000_000,
800_000,
);
let doc = roundtrip(writer);
let text = doc.plain_text();
assert!(text.contains("Big"));
assert!(text.contains("label"));
}
}