ppt_rs/export/
pdf_export.rs1use crate::api::Presentation;
33use crate::exc::{PptxError, Result};
34use std::path::Path;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum PdfOrientation {
39 #[default]
40 Portrait,
41 Landscape,
42}
43
44#[derive(Debug, Clone)]
46pub struct PdfExportOptions {
47 pub orientation: PdfOrientation,
49 pub font: String,
52 pub font_size: f32,
54 pub include_frontmatter: bool,
57 pub include_notes: bool,
59 pub include_images: bool,
61}
62
63impl Default for PdfExportOptions {
64 fn default() -> Self {
65 Self {
66 orientation: PdfOrientation::Portrait,
67 font: "Helvetica".to_string(),
68 font_size: 12.0,
69 include_frontmatter: true,
70 include_notes: true,
71 include_images: true,
72 }
73 }
74}
75
76impl PdfExportOptions {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn with_orientation(mut self, orientation: PdfOrientation) -> Self {
84 self.orientation = orientation;
85 self
86 }
87
88 pub fn with_font(mut self, font: impl Into<String>) -> Self {
90 self.font = font.into();
91 self
92 }
93
94 pub fn with_font_size(mut self, size: f32) -> Self {
96 self.font_size = size.max(1.0);
97 self
98 }
99
100 pub fn with_frontmatter(mut self, include: bool) -> Self {
102 self.include_frontmatter = include;
103 self
104 }
105
106 pub fn with_notes(mut self, include: bool) -> Self {
108 self.include_notes = include;
109 self
110 }
111
112 pub fn with_images(mut self, include: bool) -> Self {
114 self.include_images = include;
115 self
116 }
117
118 pub fn landscape() -> Self {
120 Self::default().with_orientation(PdfOrientation::Landscape)
121 }
122}
123
124pub fn export_to_pdf_bytes(
128 presentation: &Presentation,
129 options: &PdfExportOptions,
130) -> Result<Vec<u8>> {
131 let md = crate::export::md::export_to_markdown_with_options(
132 presentation,
133 &crate::export::md::MarkdownOptions {
134 include_frontmatter: options.include_frontmatter,
135 slide_separator: "---".to_string(),
136 include_notes: options.include_notes,
137 use_gfm_tables: true,
138 include_images: options.include_images,
139 include_slide_numbers: true,
140 },
141 )?;
142
143 let elements = pdfrs::elements::parse_markdown(&md);
144 let layout = match options.orientation {
145 PdfOrientation::Portrait => pdfrs::pdf_generator::PageLayout::portrait(),
146 PdfOrientation::Landscape => pdfrs::pdf_generator::PageLayout::landscape(),
147 };
148
149 pdfrs::pdf_generator::generate_pdf_bytes(&elements, &options.font, options.font_size, layout)
150 .map_err(|e| PptxError::Generic(format!("pdfrs generation failed: {e}")))
151}
152
153pub fn export_to_pdf<P: AsRef<Path>>(
155 presentation: &Presentation,
156 output_path: P,
157 options: &PdfExportOptions,
158) -> Result<Vec<u8>> {
159 let bytes = export_to_pdf_bytes(presentation, options)?;
160 std::fs::write(output_path.as_ref(), &bytes)?;
161 Ok(bytes)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::generator::SlideContent;
168
169 #[test]
170 fn test_options_default() {
171 let opts = PdfExportOptions::default();
172 assert_eq!(opts.orientation, PdfOrientation::Portrait);
173 assert_eq!(opts.font, "Helvetica");
174 assert!((opts.font_size - 12.0).abs() < f32::EPSILON);
175 assert!(opts.include_frontmatter);
176 assert!(opts.include_notes);
177 assert!(opts.include_images);
178 }
179
180 #[test]
181 fn test_options_builder() {
182 let opts = PdfExportOptions::new()
183 .with_orientation(PdfOrientation::Landscape)
184 .with_font("Times-Roman")
185 .with_font_size(10.0)
186 .with_frontmatter(false)
187 .with_notes(false)
188 .with_images(false);
189
190 assert_eq!(opts.orientation, PdfOrientation::Landscape);
191 assert_eq!(opts.font, "Times-Roman");
192 assert!((opts.font_size - 10.0).abs() < f32::EPSILON);
193 assert!(!opts.include_frontmatter);
194 assert!(!opts.include_notes);
195 assert!(!opts.include_images);
196 }
197
198 #[test]
199 fn test_options_landscape_preset() {
200 let opts = PdfExportOptions::landscape();
201 assert_eq!(opts.orientation, PdfOrientation::Landscape);
202 }
203
204 #[test]
205 fn test_options_font_size_floors_at_one() {
206 let opts = PdfExportOptions::new().with_font_size(0.0);
207 assert!(opts.font_size >= 1.0);
208 }
209
210 #[test]
211 fn test_export_to_pdf_bytes_simple() {
212 let pres = Presentation::with_title("Native PDF")
213 .add_slide(SlideContent::new("Slide 1").add_bullet("Hello"))
214 .add_slide(SlideContent::new("Slide 2").add_bullet("World"));
215
216 let bytes = export_to_pdf_bytes(&pres, &PdfExportOptions::default()).unwrap();
217 assert!(!bytes.is_empty());
218 assert_eq!(&bytes[..5], b"%PDF-");
219 }
220
221 #[test]
222 fn test_export_to_pdf_bytes_landscape() {
223 let pres = Presentation::with_title("Landscape")
224 .add_slide(SlideContent::new("Only"));
225
226 let opts = PdfExportOptions::landscape();
227 let bytes = export_to_pdf_bytes(&pres, &opts).unwrap();
228 assert_eq!(&bytes[..5], b"%PDF-");
229 }
230
231 #[test]
232 fn test_export_to_pdf_file() {
233 let pres = Presentation::with_title("File")
234 .add_slide(SlideContent::new("Hi").add_bullet("Bullet"));
235
236 let path = std::env::temp_dir().join(format!(
237 "ppt_rs_pdf_export_{}.pdf",
238 uuid::Uuid::new_v4()
239 ));
240 let opts = PdfExportOptions::new().with_frontmatter(false).with_notes(false);
241
242 export_to_pdf(&pres, &path, &opts).unwrap();
243 let read_back = std::fs::read(&path).unwrap();
244 assert_eq!(&read_back[..5], b"%PDF-");
245 assert!(pdfrs::pdf::validate_pdf_bytes(&read_back).valid);
246
247 let _ = std::fs::remove_file(&path);
248 }
249
250 #[test]
251 fn test_export_to_pdf_bytes_without_frontmatter() {
252 let pres = Presentation::with_title("NoFrontmatter")
253 .add_slide(SlideContent::new("S1").add_bullet("A"));
254
255 let opts = PdfExportOptions::new().with_frontmatter(false);
256 let bytes = export_to_pdf_bytes(&pres, &opts).unwrap();
257 assert_eq!(&bytes[..5], b"%PDF-");
258 }
259
260 #[test]
261 fn test_export_empty_presentation_still_produces_pdf() {
262 let pres = Presentation::with_title("Empty");
263 let bytes = export_to_pdf_bytes(&pres, &PdfExportOptions::default()).unwrap();
264 assert_eq!(&bytes[..5], b"%PDF-");
265 }
266
267 #[test]
268 fn test_orientation_default_is_portrait() {
269 assert_eq!(PdfOrientation::default(), PdfOrientation::Portrait);
270 }
271}