use imagesize;
use crate::enums::ShapeType;
use crate::error::PptxError;
use crate::objects::{SlideObject, SlideRel, SlideRelMedia, TextObject, ShapeObject, ImageObject, ConnectorObject, ConnectorType, ConnectorOptions};
use crate::objects::text::{TextRun, TextRunOptions};
use crate::objects::shape::ShapeOptions;
use crate::objects::image::ImageOptions;
use crate::packaging::assemble_pptx;
use crate::slide::Slide;
use crate::types::{Coord, PresLayout};
#[derive(Debug, Clone)]
pub struct SlideLayout {
pub name: String,
pub width: Option<f64>,
pub height: Option<f64>,
}
#[derive(Debug, Clone, Default)]
pub struct SlideMasterDef {
pub title: String,
pub background_color: Option<String>,
pub background_transparency: Option<f64>,
pub(crate) background_image_rid: Option<u32>,
pub objects: Vec<SlideObject>,
pub(crate) rels: Vec<SlideRel>,
pub(crate) rels_media: Vec<SlideRelMedia>,
pub(crate) obj_counter: u32,
}
impl SlideMasterDef {
pub fn new(title: impl Into<String>) -> Self {
SlideMasterDef {
title: title.into(),
..Default::default()
}
}
fn next_obj_name(&mut self, prefix: &str) -> String {
self.obj_counter += 1;
format!("{} {}", prefix, self.obj_counter)
}
fn allocate_rid(&self) -> u32 {
(self.rels.len() + self.rels_media.len() + 1) as u32
}
fn register_hyperlink_rel(&mut self, hl: &mut crate::types::HyperlinkProps) {
if hl.action.is_none() {
let rid = self.allocate_rid();
hl.r_id = rid;
let (rel_type, target, data_field) = if let Some(slide) = hl.slide {
("hyperlink".to_string(), slide.to_string(), Some("slide".to_string()))
} else if let Some(ref url) = hl.url {
("hyperlink".to_string(), url.clone(), None)
} else {
("hyperlink".to_string(), String::new(), None)
};
self.rels.push(SlideRel { r_id: rid, rel_type, target, data: data_field });
}
}
pub fn set_background_color(&mut self, color: impl Into<String>) -> &mut Self {
self.background_color = Some(color.into().trim_start_matches('#').to_uppercase());
self
}
pub fn set_background_color_transparency(&mut self, color: impl Into<String>, transparency: f64) -> &mut Self {
self.background_color = Some(color.into().trim_start_matches('#').to_uppercase());
self.background_transparency = Some(transparency);
self
}
pub fn set_background_image(&mut self, data: Vec<u8>, extension: &str) -> &mut Self {
let rid = self.allocate_rid();
let extn = extension.to_lowercase();
let target = format!("../media/image{}.{}", rid, extn);
self.rels_media.push(SlideRelMedia {
r_id: rid,
rel_type: "image".to_string(),
target,
extn,
data,
});
self.background_image_rid = Some(rid);
self
}
pub fn add_text(&mut self, text: impl Into<String>, opts: crate::objects::text::TextOptions) -> &mut Self {
let name = self.next_obj_name("TextBox");
let run = TextRun { text: text.into(), options: TextRunOptions::default(), break_line: false, soft_break_before: false, field: None, equation_omml: None };
let obj = TextObject { object_name: name, text: vec![run], options: opts };
self.objects.push(SlideObject::Text(obj));
self
}
pub fn add_text_runs(&mut self, runs: Vec<TextRun>, opts: crate::objects::text::TextOptions) -> &mut Self {
let name = self.next_obj_name("TextBox");
let mut runs = runs;
for run in &mut runs {
if let Some(ref mut hl) = run.options.hyperlink {
self.register_hyperlink_rel(hl);
}
}
let obj = TextObject { object_name: name, text: runs, options: opts };
self.objects.push(SlideObject::Text(obj));
self
}
pub fn add_shape(&mut self, shape_type: ShapeType, mut opts: ShapeOptions) -> &mut Self {
if let Some(ref mut hl) = opts.hyperlink {
self.register_hyperlink_rel(hl);
}
if let Some(ref mut hl) = opts.hover {
self.register_hyperlink_rel(hl);
}
let name = self.next_obj_name("Shape");
let obj = ShapeObject { object_name: name, shape_type, options: opts, text: None };
self.objects.push(SlideObject::Shape(obj));
self
}
pub fn add_shape_with_text(
&mut self,
shape_type: ShapeType,
opts: ShapeOptions,
text: impl Into<String>,
text_opts: crate::objects::text::TextOptions,
) -> &mut Self {
let shape_name = self.next_obj_name("Shape");
let text_name = format!("{} Text", shape_name);
let run = TextRun { text: text.into(), options: TextRunOptions::default(), break_line: false, soft_break_before: false, field: None, equation_omml: None };
let text_obj = TextObject { object_name: text_name, text: vec![run], options: text_opts };
let obj = ShapeObject { object_name: shape_name, shape_type, options: opts, text: Some(text_obj) };
self.objects.push(SlideObject::Shape(obj));
self
}
pub fn add_image(&mut self, data: Vec<u8>, extension: &str, mut opts: ImageOptions) -> &mut Self {
let name = self.next_obj_name("Image");
if opts.position.w.is_none() || opts.position.h.is_none() {
if let Ok(dim) = imagesize::blob_size(&data) {
let emu_w = (dim.width as i64) * 914_400 / 96;
let emu_h = (dim.height as i64) * 914_400 / 96;
if opts.position.w.is_none() { opts.position.w = Some(Coord::Emu(emu_w)); }
if opts.position.h.is_none() { opts.position.h = Some(Coord::Emu(emu_h)); }
}
}
if let Some(ref mut hl) = opts.hyperlink {
self.register_hyperlink_rel(hl);
}
if let Some(ref mut hl) = opts.hover {
self.register_hyperlink_rel(hl);
}
let rid = self.allocate_rid();
let extn = extension.to_lowercase();
let is_svg = extn == "svg";
let target = format!("../media/image{}.{}", rid, extn);
self.rels_media.push(SlideRelMedia {
r_id: rid, rel_type: "image".to_string(), target, extn: extn.clone(), data: data.clone(),
});
let obj = ImageObject { object_name: name, image_rid: rid, extension: extn, data, is_svg, options: opts };
self.objects.push(SlideObject::Image(obj));
self
}
pub fn add_image_base64(&mut self, b64: &str, extension: &str, opts: ImageOptions) -> Result<&mut Self, PptxError> {
let raw = if let Some(idx) = b64.find(',') { &b64[idx + 1..] } else { b64 };
let data = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, raw)
.map_err(|e| PptxError::InvalidArgument(format!("base64 decode error: {e}")))?;
Ok(self.add_image(data, extension, opts))
}
pub fn add_connector(&mut self, connector_type: ConnectorType, opts: ConnectorOptions) -> &mut Self {
let name = self.next_obj_name("Connector");
let obj = ConnectorObject { object_name: name, connector_type, options: opts };
self.objects.push(SlideObject::Connector(obj));
self
}
pub fn add_group(&mut self, children: Vec<SlideObject>, opts: crate::objects::group::GroupOptions) -> &mut Self {
use crate::enums::EMU;
let name = self.next_obj_name("Group");
let cx = opts.position.w.as_ref().map(|c| c.to_emu(12_192_000)).unwrap_or(EMU as i64);
let cy = opts.position.h.as_ref().map(|c| c.to_emu(6_858_000)).unwrap_or(EMU as i64);
let child_extent = opts.child_extent.unwrap_or((cx, cy));
let obj = crate::objects::GroupObject {
object_name: name,
position: opts.position,
child_offset: opts.child_offset,
child_extent,
children,
};
self.objects.push(SlideObject::Group(obj));
self
}
pub fn add_table(&mut self, rows: Vec<crate::objects::table::TableRow>, opts: crate::objects::table::TableOptions) -> &mut Self {
let name = self.next_obj_name("Table");
let mut opts = opts;
if opts.object_name.is_none() {
opts.object_name = Some(name.clone());
}
let mut rows = rows;
for row in &mut rows {
for cell in row.iter_mut() {
if let Some(ref mut hl) = cell.options.hyperlink {
if hl.action.is_none() {
self.register_hyperlink_rel(hl);
}
}
}
}
let obj = crate::objects::TableObject { object_name: name, rows, options: opts };
self.objects.push(SlideObject::Table(obj));
self
}
}
#[derive(Debug, Clone, Default)]
pub struct ThemeProps {
pub head_font_face: Option<String>,
pub body_font_face: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SectionDef {
pub name: String,
pub start_slide: u32,
}
#[derive(Debug)]
pub struct Presentation {
pub author: String,
pub company: String,
pub title: String,
pub subject: String,
pub revision: String,
pub rtl_mode: bool,
pub layout: PresLayout,
pub theme: Option<ThemeProps>,
pub(crate) slides: Vec<Slide>,
pub(crate) slide_layouts: Vec<SlideLayout>,
pub(crate) master: Option<SlideMasterDef>,
pub(crate) sections: Vec<SectionDef>,
slide_id_counter: u32,
}
impl Default for Presentation {
fn default() -> Self {
Presentation {
author: String::new(),
company: String::new(),
title: String::new(),
subject: String::new(),
revision: "1".to_string(),
rtl_mode: false,
layout: PresLayout::default(),
theme: None,
slides: Vec::new(),
slide_layouts: vec![SlideLayout { name: "DEFAULT".to_string(), width: None, height: None }],
master: None,
sections: Vec::new(),
slide_id_counter: 0,
}
}
}
impl Presentation {
pub fn new() -> Self {
Presentation::default()
}
pub fn with_layout(layout: PresLayout) -> Self {
Presentation { layout, ..Default::default() }
}
pub fn author(mut self, a: impl Into<String>) -> Self { self.author = a.into(); self }
pub fn company(mut self, c: impl Into<String>) -> Self { self.company = c.into(); self }
pub fn title(mut self, t: impl Into<String>) -> Self { self.title = t.into(); self }
pub fn subject(mut self, s: impl Into<String>) -> Self { self.subject = s.into(); self }
pub fn revision(mut self, r: impl Into<String>) -> Self { self.revision = r.into(); self }
pub fn rtl(mut self) -> Self { self.rtl_mode = true; self }
pub fn theme(mut self, t: ThemeProps) -> Self { self.theme = Some(t); self }
pub fn define_layout(&mut self, name: impl Into<String>, width: Option<f64>, height: Option<f64>) -> &mut Self {
self.slide_layouts.push(SlideLayout { name: name.into(), width, height });
self
}
pub fn define_master(&mut self, def: SlideMasterDef) -> &mut Self {
self.master = Some(def);
self
}
pub fn promote_slide_to_master(&mut self, idx: usize, title: impl Into<String>) -> SlideMasterDef {
let slide = self.slides.remove(idx);
slide.into_master(title)
}
pub fn add_section(&mut self, name: impl Into<String>) -> &mut Self {
let next_slide_num = self.slide_id_counter + 1;
self.sections.push(SectionDef { name: name.into(), start_slide: next_slide_num });
self
}
pub fn add_slide(&mut self) -> &mut Slide {
self.slide_id_counter += 1;
let slide_num = self.slide_id_counter;
let slide_id = 255 + slide_num; let r_id = slide_num + 1; let slide = Slide::new(slide_num, slide_id, r_id);
self.slides.push(slide);
self.slides.last_mut().unwrap()
}
pub fn slide_count(&self) -> usize {
self.slides.len()
}
pub fn slide(&self, idx: usize) -> Option<&Slide> {
self.slides.get(idx)
}
pub fn slide_mut(&mut self, idx: usize) -> Option<&mut Slide> {
self.slides.get_mut(idx)
}
pub fn write(&self) -> Result<Vec<u8>, PptxError> {
assemble_pptx(self)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn write_to_file(&self, path: &str) -> Result<(), PptxError> {
let bytes = self.write()?;
std::fs::write(path, &bytes).map_err(PptxError::Io)
}
pub fn write_validated(&self) -> Result<Vec<u8>, PptxError> {
let bytes = self.write()?;
let issues = crate::validate::validate(&bytes);
let errors: Vec<_> = issues.into_iter()
.filter(|i| i.severity == crate::validate::Severity::Error)
.collect();
if !errors.is_empty() {
return Err(PptxError::ValidationFailed(errors));
}
Ok(bytes)
}
}