1use crate::error::Result;
12use pdf_extract::{ExtractedImage, ImageFilter, TextBlock};
13use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
14use quick_xml::Writer;
15use std::collections::HashSet;
16use std::io::{Cursor, Seek, Write};
17use zip::write::SimpleFileOptions;
18use zip::ZipWriter;
19
20const EMU_PER_PT: i64 = 12700;
22
23const SLIDE_WIDTH: i64 = 9144000;
25
26const SLIDE_HEIGHT: i64 = 6858000;
28
29#[derive(Debug)]
31pub struct PptxImage {
32 pub data: Vec<u8>,
33 pub width: u32,
34 pub height: u32,
35 pub content_type: String,
36 pub filename: String,
37}
38
39pub struct SlideData {
41 pub text_blocks: Vec<TextBlock>,
42 pub images: Vec<PptxImage>,
43 pub page_width: f64,
45 pub page_height: f64,
47}
48
49pub struct PptxWriter<W: Write + Seek> {
51 zip: std::panic::AssertUnwindSafe<ZipWriter<W>>,
54 slide_count: usize,
55 image_extensions: HashSet<String>,
56 global_image_counter: usize,
57 options: SimpleFileOptions,
58 cached_master_xml: Vec<u8>,
59 cached_master_rels_xml: Vec<u8>,
60 cached_layout_xml: Vec<u8>,
61 cached_layout_rels_xml: Vec<u8>,
62}
63
64impl<W: Write + Seek> PptxWriter<W> {
65 pub fn new(inner: W) -> Self {
66 let zip = std::panic::AssertUnwindSafe(ZipWriter::new(inner));
67 let options =
68 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
69 let cached_master_xml = write_slide_master().unwrap_or_default();
70 let cached_master_rels_xml = write_slide_master_rels().unwrap_or_default();
71 let cached_layout_xml = write_slide_layout().unwrap_or_default();
72 let cached_layout_rels_xml = write_slide_layout_rels().unwrap_or_default();
73 Self {
74 zip,
75 slide_count: 0,
76 image_extensions: HashSet::new(),
77 global_image_counter: 0,
78 options,
79 cached_master_xml,
80 cached_master_rels_xml,
81 cached_layout_xml,
82 cached_layout_rels_xml,
83 }
84 }
85
86 pub fn begin(&mut self) -> Result<()> {
88 self.zip.start_file("_rels/.rels", self.options)?;
90 self.zip.write_all(&write_root_rels()?)?;
91
92 self.zip
94 .start_file("ppt/slideMasters/slideMaster1.xml", self.options)?;
95 self.zip.write_all(&self.cached_master_xml)?;
96
97 self.zip
98 .start_file("ppt/slideMasters/_rels/slideMaster1.xml.rels", self.options)?;
99 self.zip.write_all(&self.cached_master_rels_xml)?;
100
101 self.zip
102 .start_file("ppt/slideLayouts/slideLayout1.xml", self.options)?;
103 self.zip.write_all(&self.cached_layout_xml)?;
104
105 self.zip
106 .start_file("ppt/slideLayouts/_rels/slideLayout1.xml.rels", self.options)?;
107 self.zip.write_all(&self.cached_layout_rels_xml)?;
108
109 Ok(())
110 }
111
112 pub fn add_slide(&mut self, slide: &SlideData) -> Result<()> {
114 self.slide_count += 1;
115 let slide_num = self.slide_count;
116
117 let slide_path = format!("ppt/slides/slide{slide_num}.xml");
119 self.zip.start_file(slide_path, self.options)?;
120 self.zip
121 .write_all(&write_slide(slide, self.global_image_counter)?)?;
122
123 let rels_path = format!("ppt/slides/_rels/slide{slide_num}.xml.rels");
125 self.zip.start_file(rels_path, self.options)?;
126 self.zip
127 .write_all(&write_slide_rels(slide, self.global_image_counter)?)?;
128
129 for img in &slide.images {
131 let ext = image_ext(&img.content_type);
132 self.image_extensions.insert(ext.to_string());
133
134 let img_path = format!("ppt/media/{}", img.filename);
135 self.zip.start_file(img_path, self.options)?;
136 self.zip.write_all(&img.data)?;
137 }
138
139 self.global_image_counter += slide.images.len();
140 Ok(())
141 }
142
143 pub fn finish(mut self) -> Result<W> {
145 self.zip.start_file("[Content_Types].xml", self.options)?;
147 self.zip.write_all(&write_content_types_inc(
148 self.slide_count,
149 &self.image_extensions,
150 )?)?;
151
152 self.zip.start_file("ppt/presentation.xml", self.options)?;
154 self.zip.write_all(&write_presentation(self.slide_count)?)?;
155
156 self.zip
158 .start_file("ppt/_rels/presentation.xml.rels", self.options)?;
159 self.zip
160 .write_all(&write_presentation_rels_inc(self.slide_count)?)?;
161
162 let inner = self.zip.0.finish()?;
163 Ok(inner)
164 }
165}
166
167pub fn write_pptx(slides: &[SlideData], output: &mut Vec<u8>) -> Result<()> {
169 let mut writer = PptxWriter::new(Cursor::new(Vec::new()));
170 writer.begin()?;
171 for slide in slides {
172 writer.add_slide(slide)?;
173 }
174 let cursor = writer.finish()?;
175 *output = cursor.into_inner();
176 Ok(())
177}
178
179fn xml_decl(w: &mut Writer<&mut Cursor<Vec<u8>>>) -> Result<()> {
182 w.write_event(Event::Decl(BytesDecl::new(
183 "1.0",
184 Some("UTF-8"),
185 Some("yes"),
186 )))?;
187 Ok(())
188}
189
190fn write_content_types_inc(slide_count: usize, image_exts: &HashSet<String>) -> Result<Vec<u8>> {
191 let mut buf = Cursor::new(Vec::new());
192 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
193 xml_decl(&mut w)?;
194
195 let mut types = BytesStart::new("Types");
196 types.push_attribute((
197 "xmlns",
198 "http://schemas.openxmlformats.org/package/2006/content-types",
199 ));
200 w.write_event(Event::Start(types))?;
201
202 empty_with_attrs(
204 &mut w,
205 "Default",
206 &[
207 ("Extension", "rels"),
208 (
209 "ContentType",
210 "application/vnd.openxmlformats-package.relationships+xml",
211 ),
212 ],
213 )?;
214 empty_with_attrs(
215 &mut w,
216 "Default",
217 &[("Extension", "xml"), ("ContentType", "application/xml")],
218 )?;
219
220 for ext in image_exts {
222 let content_type = match ext.as_str() {
223 "jpeg" | "jpg" => "image/jpeg",
224 "png" => "image/png",
225 "gif" => "image/gif",
226 "tiff" => "image/tiff",
227 _ => "image/png",
228 };
229 empty_with_attrs(
230 &mut w,
231 "Default",
232 &[("Extension", ext), ("ContentType", content_type)],
233 )?;
234 }
235
236 empty_with_attrs(&mut w, "Override", &[
238 ("PartName", "/ppt/presentation.xml"),
239 ("ContentType", "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"),
240 ])?;
241 empty_with_attrs(
242 &mut w,
243 "Override",
244 &[
245 ("PartName", "/ppt/slideMasters/slideMaster1.xml"),
246 (
247 "ContentType",
248 "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml",
249 ),
250 ],
251 )?;
252 empty_with_attrs(
253 &mut w,
254 "Override",
255 &[
256 ("PartName", "/ppt/slideLayouts/slideLayout1.xml"),
257 (
258 "ContentType",
259 "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml",
260 ),
261 ],
262 )?;
263
264 for i in 1..=slide_count {
265 let part = format!("/ppt/slides/slide{i}.xml");
266 empty_with_attrs(
267 &mut w,
268 "Override",
269 &[
270 ("PartName", &part),
271 (
272 "ContentType",
273 "application/vnd.openxmlformats-officedocument.presentationml.slide+xml",
274 ),
275 ],
276 )?;
277 }
278
279 w.write_event(Event::End(BytesEnd::new("Types")))?;
280 Ok(buf.into_inner())
281}
282
283fn write_root_rels() -> Result<Vec<u8>> {
284 let mut buf = Cursor::new(Vec::new());
285 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
286 xml_decl(&mut w)?;
287
288 let mut rels = BytesStart::new("Relationships");
289 rels.push_attribute((
290 "xmlns",
291 "http://schemas.openxmlformats.org/package/2006/relationships",
292 ));
293 w.write_event(Event::Start(rels))?;
294
295 empty_with_attrs(&mut w, "Relationship", &[
296 ("Id", "rId1"),
297 ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"),
298 ("Target", "ppt/presentation.xml"),
299 ])?;
300
301 w.write_event(Event::End(BytesEnd::new("Relationships")))?;
302 Ok(buf.into_inner())
303}
304
305fn write_presentation(slide_count: usize) -> Result<Vec<u8>> {
306 let mut buf = Cursor::new(Vec::new());
307 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
308 xml_decl(&mut w)?;
309
310 let mut pres = BytesStart::new("p:presentation");
311 pres.push_attribute((
312 "xmlns:a",
313 "http://schemas.openxmlformats.org/drawingml/2006/main",
314 ));
315 pres.push_attribute((
316 "xmlns:p",
317 "http://schemas.openxmlformats.org/presentationml/2006/main",
318 ));
319 pres.push_attribute((
320 "xmlns:r",
321 "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
322 ));
323 w.write_event(Event::Start(pres))?;
324
325 w.write_event(Event::Start(BytesStart::new("p:sldMasterIdLst")))?;
327 let mut smid = BytesStart::new("p:sldMasterId");
328 smid.push_attribute(("id", "2147483648"));
329 smid.push_attribute(("r:id", "rId1"));
330 w.write_event(Event::Empty(smid))?;
331 w.write_event(Event::End(BytesEnd::new("p:sldMasterIdLst")))?;
332
333 w.write_event(Event::Start(BytesStart::new("p:sldIdLst")))?;
335 for i in 0..slide_count {
336 let id = (256 + i).to_string();
337 let rid = format!("rId{}", i + 2);
338 let mut sid = BytesStart::new("p:sldId");
339 sid.push_attribute(("id", id.as_str()));
340 sid.push_attribute(("r:id", rid.as_str()));
341 w.write_event(Event::Empty(sid))?;
342 }
343 w.write_event(Event::End(BytesEnd::new("p:sldIdLst")))?;
344
345 let w_str = SLIDE_WIDTH.to_string();
347 let h_str = SLIDE_HEIGHT.to_string();
348 let mut ssz = BytesStart::new("p:sldSz");
349 ssz.push_attribute(("cx", w_str.as_str()));
350 ssz.push_attribute(("cy", h_str.as_str()));
351 ssz.push_attribute(("type", "screen4x3"));
352 w.write_event(Event::Empty(ssz))?;
353
354 let mut nsz = BytesStart::new("p:notesSz");
355 nsz.push_attribute(("cx", "6858000"));
356 nsz.push_attribute(("cy", "9144000"));
357 w.write_event(Event::Empty(nsz))?;
358
359 w.write_event(Event::End(BytesEnd::new("p:presentation")))?;
360 Ok(buf.into_inner())
361}
362
363fn write_presentation_rels_inc(slide_count: usize) -> Result<Vec<u8>> {
364 let mut buf = Cursor::new(Vec::new());
365 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
366 xml_decl(&mut w)?;
367
368 let mut rels = BytesStart::new("Relationships");
369 rels.push_attribute((
370 "xmlns",
371 "http://schemas.openxmlformats.org/package/2006/relationships",
372 ));
373 w.write_event(Event::Start(rels))?;
374
375 empty_with_attrs(
377 &mut w,
378 "Relationship",
379 &[
380 ("Id", "rId1"),
381 (
382 "Type",
383 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster",
384 ),
385 ("Target", "slideMasters/slideMaster1.xml"),
386 ],
387 )?;
388
389 for i in 0..slide_count {
391 let rid = format!("rId{}", i + 2);
392 let target = format!("slides/slide{}.xml", i + 1);
393 empty_with_attrs(
394 &mut w,
395 "Relationship",
396 &[
397 ("Id", &rid),
398 (
399 "Type",
400 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide",
401 ),
402 ("Target", &target),
403 ],
404 )?;
405 }
406
407 w.write_event(Event::End(BytesEnd::new("Relationships")))?;
408 Ok(buf.into_inner())
409}
410
411fn write_slide_master() -> Result<Vec<u8>> {
412 let mut buf = Cursor::new(Vec::new());
413 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
414 xml_decl(&mut w)?;
415
416 let mut sm = BytesStart::new("p:sldMaster");
417 sm.push_attribute((
418 "xmlns:a",
419 "http://schemas.openxmlformats.org/drawingml/2006/main",
420 ));
421 sm.push_attribute((
422 "xmlns:p",
423 "http://schemas.openxmlformats.org/presentationml/2006/main",
424 ));
425 sm.push_attribute((
426 "xmlns:r",
427 "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
428 ));
429 w.write_event(Event::Start(sm))?;
430
431 w.write_event(Event::Start(BytesStart::new("p:cSld")))?;
432 w.write_event(Event::Start(BytesStart::new("p:spTree")))?;
433
434 w.write_event(Event::Start(BytesStart::new("p:nvGrpSpPr")))?;
436 let mut cnv = BytesStart::new("p:cNvPr");
437 cnv.push_attribute(("id", "1"));
438 cnv.push_attribute(("name", ""));
439 w.write_event(Event::Empty(cnv))?;
440 w.write_event(Event::Empty(BytesStart::new("p:cNvGrpSpPr")))?;
441 w.write_event(Event::Empty(BytesStart::new("p:nvPr")))?;
442 w.write_event(Event::End(BytesEnd::new("p:nvGrpSpPr")))?;
443
444 w.write_event(Event::Start(BytesStart::new("p:grpSpPr")))?;
445 write_xfrm_group(&mut w)?;
446 w.write_event(Event::End(BytesEnd::new("p:grpSpPr")))?;
447
448 w.write_event(Event::End(BytesEnd::new("p:spTree")))?;
449 w.write_event(Event::End(BytesEnd::new("p:cSld")))?;
450
451 w.write_event(Event::Start(BytesStart::new("p:sldLayoutIdLst")))?;
453 let mut lid = BytesStart::new("p:sldLayoutId");
454 lid.push_attribute(("id", "2147483649"));
455 lid.push_attribute(("r:id", "rId1"));
456 w.write_event(Event::Empty(lid))?;
457 w.write_event(Event::End(BytesEnd::new("p:sldLayoutIdLst")))?;
458
459 w.write_event(Event::End(BytesEnd::new("p:sldMaster")))?;
460 Ok(buf.into_inner())
461}
462
463fn write_slide_master_rels() -> Result<Vec<u8>> {
464 let mut buf = Cursor::new(Vec::new());
465 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
466 xml_decl(&mut w)?;
467
468 let mut rels = BytesStart::new("Relationships");
469 rels.push_attribute((
470 "xmlns",
471 "http://schemas.openxmlformats.org/package/2006/relationships",
472 ));
473 w.write_event(Event::Start(rels))?;
474
475 empty_with_attrs(
476 &mut w,
477 "Relationship",
478 &[
479 ("Id", "rId1"),
480 (
481 "Type",
482 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout",
483 ),
484 ("Target", "../slideLayouts/slideLayout1.xml"),
485 ],
486 )?;
487
488 w.write_event(Event::End(BytesEnd::new("Relationships")))?;
489 Ok(buf.into_inner())
490}
491
492fn write_slide_layout() -> Result<Vec<u8>> {
493 let mut buf = Cursor::new(Vec::new());
494 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
495 xml_decl(&mut w)?;
496
497 let mut sl = BytesStart::new("p:sldLayout");
498 sl.push_attribute((
499 "xmlns:a",
500 "http://schemas.openxmlformats.org/drawingml/2006/main",
501 ));
502 sl.push_attribute((
503 "xmlns:p",
504 "http://schemas.openxmlformats.org/presentationml/2006/main",
505 ));
506 sl.push_attribute((
507 "xmlns:r",
508 "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
509 ));
510 sl.push_attribute(("type", "blank"));
511 w.write_event(Event::Start(sl))?;
512
513 w.write_event(Event::Start(BytesStart::new("p:cSld")))?;
514 w.write_event(Event::Start(BytesStart::new("p:spTree")))?;
515
516 w.write_event(Event::Start(BytesStart::new("p:nvGrpSpPr")))?;
517 let mut cnv = BytesStart::new("p:cNvPr");
518 cnv.push_attribute(("id", "1"));
519 cnv.push_attribute(("name", ""));
520 w.write_event(Event::Empty(cnv))?;
521 w.write_event(Event::Empty(BytesStart::new("p:cNvGrpSpPr")))?;
522 w.write_event(Event::Empty(BytesStart::new("p:nvPr")))?;
523 w.write_event(Event::End(BytesEnd::new("p:nvGrpSpPr")))?;
524
525 w.write_event(Event::Start(BytesStart::new("p:grpSpPr")))?;
526 write_xfrm_group(&mut w)?;
527 w.write_event(Event::End(BytesEnd::new("p:grpSpPr")))?;
528
529 w.write_event(Event::End(BytesEnd::new("p:spTree")))?;
530 w.write_event(Event::End(BytesEnd::new("p:cSld")))?;
531
532 w.write_event(Event::End(BytesEnd::new("p:sldLayout")))?;
533 Ok(buf.into_inner())
534}
535
536fn write_slide_layout_rels() -> Result<Vec<u8>> {
537 let mut buf = Cursor::new(Vec::new());
538 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
539 xml_decl(&mut w)?;
540
541 let mut rels = BytesStart::new("Relationships");
542 rels.push_attribute((
543 "xmlns",
544 "http://schemas.openxmlformats.org/package/2006/relationships",
545 ));
546 w.write_event(Event::Start(rels))?;
547
548 empty_with_attrs(
549 &mut w,
550 "Relationship",
551 &[
552 ("Id", "rId1"),
553 (
554 "Type",
555 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster",
556 ),
557 ("Target", "../slideMasters/slideMaster1.xml"),
558 ],
559 )?;
560
561 w.write_event(Event::End(BytesEnd::new("Relationships")))?;
562 Ok(buf.into_inner())
563}
564
565fn write_slide(slide: &SlideData, _img_offset: usize) -> Result<Vec<u8>> {
566 let mut buf = Cursor::new(Vec::new());
567 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
568 xml_decl(&mut w)?;
569
570 let mut sld = BytesStart::new("p:sld");
571 sld.push_attribute((
572 "xmlns:a",
573 "http://schemas.openxmlformats.org/drawingml/2006/main",
574 ));
575 sld.push_attribute((
576 "xmlns:p",
577 "http://schemas.openxmlformats.org/presentationml/2006/main",
578 ));
579 sld.push_attribute((
580 "xmlns:r",
581 "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
582 ));
583 w.write_event(Event::Start(sld))?;
584
585 w.write_event(Event::Start(BytesStart::new("p:cSld")))?;
586 w.write_event(Event::Start(BytesStart::new("p:spTree")))?;
587
588 w.write_event(Event::Start(BytesStart::new("p:nvGrpSpPr")))?;
590 let mut cnv = BytesStart::new("p:cNvPr");
591 cnv.push_attribute(("id", "1"));
592 cnv.push_attribute(("name", ""));
593 w.write_event(Event::Empty(cnv))?;
594 w.write_event(Event::Empty(BytesStart::new("p:cNvGrpSpPr")))?;
595 w.write_event(Event::Empty(BytesStart::new("p:nvPr")))?;
596 w.write_event(Event::End(BytesEnd::new("p:nvGrpSpPr")))?;
597
598 w.write_event(Event::Start(BytesStart::new("p:grpSpPr")))?;
599 write_xfrm_group(&mut w)?;
600 w.write_event(Event::End(BytesEnd::new("p:grpSpPr")))?;
601
602 let sx = SLIDE_WIDTH as f64 / (slide.page_width * EMU_PER_PT as f64 / EMU_PER_PT as f64);
604 let sy = SLIDE_HEIGHT as f64 / (slide.page_height * EMU_PER_PT as f64 / EMU_PER_PT as f64);
605 let scale = sx.min(sy);
606
607 let mut shape_id = 2u32;
609
610 for block in &slide.text_blocks {
612 write_text_shape(&mut w, block, slide.page_height, scale, shape_id)?;
613 shape_id += 1;
614 }
615
616 for (i, img) in slide.images.iter().enumerate() {
618 let rid = format!("rId{}", i + 2); write_image_shape(&mut w, img, &rid, shape_id)?;
620 shape_id += 1;
621 }
622
623 w.write_event(Event::End(BytesEnd::new("p:spTree")))?;
624 w.write_event(Event::End(BytesEnd::new("p:cSld")))?;
625 w.write_event(Event::End(BytesEnd::new("p:sld")))?;
626 Ok(buf.into_inner())
627}
628
629fn write_slide_rels(slide: &SlideData, img_offset: usize) -> Result<Vec<u8>> {
630 let mut buf = Cursor::new(Vec::new());
631 let mut w = Writer::new_with_indent(&mut buf, b' ', 2);
632 xml_decl(&mut w)?;
633
634 let mut rels = BytesStart::new("Relationships");
635 rels.push_attribute((
636 "xmlns",
637 "http://schemas.openxmlformats.org/package/2006/relationships",
638 ));
639 w.write_event(Event::Start(rels))?;
640
641 empty_with_attrs(
643 &mut w,
644 "Relationship",
645 &[
646 ("Id", "rId1"),
647 (
648 "Type",
649 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout",
650 ),
651 ("Target", "../slideLayouts/slideLayout1.xml"),
652 ],
653 )?;
654
655 for (i, img) in slide.images.iter().enumerate() {
657 let rid = format!("rId{}", i + 2);
658 let ext = image_ext(&img.content_type);
660 let target = format!("../media/image{}.{}", img_offset + i, ext);
661 empty_with_attrs(
662 &mut w,
663 "Relationship",
664 &[
665 ("Id", &rid),
666 (
667 "Type",
668 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
669 ),
670 ("Target", &target),
671 ],
672 )?;
673 }
674
675 w.write_event(Event::End(BytesEnd::new("Relationships")))?;
676 Ok(buf.into_inner())
677}
678
679fn write_text_shape(
681 w: &mut Writer<&mut Cursor<Vec<u8>>>,
682 block: &TextBlock,
683 page_height: f64,
684 scale: f64,
685 shape_id: u32,
686) -> Result<()> {
687 let x_emu = (block.bbox[0] * EMU_PER_PT as f64 * scale / EMU_PER_PT as f64) as i64;
689 let y_pdf = page_height - block.bbox[3]; let y_emu = (y_pdf * EMU_PER_PT as f64 * scale / EMU_PER_PT as f64) as i64;
691 let w_emu =
692 ((block.bbox[2] - block.bbox[0]) * EMU_PER_PT as f64 * scale / EMU_PER_PT as f64) as i64;
693 let h_emu = (block.font_size * EMU_PER_PT as f64 * scale / EMU_PER_PT as f64 * 1.4) as i64;
694
695 let x_emu = x_emu.max(0);
696 let y_emu = y_emu.max(0);
697 let w_emu = w_emu.max(EMU_PER_PT);
698 let h_emu = h_emu.max(EMU_PER_PT);
699
700 w.write_event(Event::Start(BytesStart::new("p:sp")))?;
701
702 w.write_event(Event::Start(BytesStart::new("p:nvSpPr")))?;
704 let name = format!("TextBox {shape_id}");
705 let id_str = shape_id.to_string();
706 let mut cnv = BytesStart::new("p:cNvPr");
707 cnv.push_attribute(("id", id_str.as_str()));
708 cnv.push_attribute(("name", name.as_str()));
709 w.write_event(Event::Empty(cnv))?;
710
711 let mut cnv_sp = BytesStart::new("p:cNvSpPr");
712 cnv_sp.push_attribute(("txBox", "1"));
713 w.write_event(Event::Empty(cnv_sp))?;
714
715 w.write_event(Event::Empty(BytesStart::new("p:nvPr")))?;
716 w.write_event(Event::End(BytesEnd::new("p:nvSpPr")))?;
717
718 w.write_event(Event::Start(BytesStart::new("p:spPr")))?;
720 write_xfrm_positioned(w, x_emu, y_emu, w_emu, h_emu)?;
721
722 let mut prst = BytesStart::new("a:prstGeom");
723 prst.push_attribute(("prst", "rect"));
724 w.write_event(Event::Start(prst))?;
725 w.write_event(Event::Empty(BytesStart::new("a:avLst")))?;
726 w.write_event(Event::End(BytesEnd::new("a:prstGeom")))?;
727
728 w.write_event(Event::Empty(BytesStart::new("a:noFill")))?;
729 w.write_event(Event::End(BytesEnd::new("p:spPr")))?;
730
731 w.write_event(Event::Start(BytesStart::new("p:txBody")))?;
733
734 let mut body_pr = BytesStart::new("a:bodyPr");
735 body_pr.push_attribute(("wrap", "square"));
736 w.write_event(Event::Empty(body_pr))?;
737
738 w.write_event(Event::Empty(BytesStart::new("a:lstStyle")))?;
739
740 w.write_event(Event::Start(BytesStart::new("a:p")))?;
742 w.write_event(Event::Start(BytesStart::new("a:r")))?;
743
744 let font_size_hundredths = ((block.font_size * scale) * 100.0) as i64;
746 let sz_str = font_size_hundredths.to_string();
747 let mut rpr = BytesStart::new("a:rPr");
748 rpr.push_attribute(("lang", "en-US"));
749 rpr.push_attribute(("sz", sz_str.as_str()));
750
751 let is_bold = block.font_name.contains("Bold") || block.font_name.contains("bold");
752 let is_italic = block.font_name.contains("Italic")
753 || block.font_name.contains("italic")
754 || block.font_name.contains("Oblique");
755
756 if is_bold {
757 rpr.push_attribute(("b", "1"));
758 }
759 if is_italic {
760 rpr.push_attribute(("i", "1"));
761 }
762
763 w.write_event(Event::Empty(rpr))?;
764
765 w.write_event(Event::Start(BytesStart::new("a:t")))?;
766 w.write_event(Event::Text(BytesText::new(&block.text)))?;
767 w.write_event(Event::End(BytesEnd::new("a:t")))?;
768
769 w.write_event(Event::End(BytesEnd::new("a:r")))?;
770 w.write_event(Event::End(BytesEnd::new("a:p")))?;
771
772 w.write_event(Event::End(BytesEnd::new("p:txBody")))?;
773 w.write_event(Event::End(BytesEnd::new("p:sp")))?;
774
775 Ok(())
776}
777
778fn write_image_shape(
780 w: &mut Writer<&mut Cursor<Vec<u8>>>,
781 img: &PptxImage,
782 rid: &str,
783 shape_id: u32,
784) -> Result<()> {
785 let dpi = 96.0f64;
786 let cx = (img.width as f64 / dpi * 914400.0) as i64;
787 let cy = (img.height as f64 / dpi * 914400.0) as i64;
788
789 w.write_event(Event::Start(BytesStart::new("p:pic")))?;
790
791 w.write_event(Event::Start(BytesStart::new("p:nvPicPr")))?;
793 let name = format!("Image {shape_id}");
794 let id_str = shape_id.to_string();
795 let mut cnv = BytesStart::new("p:cNvPr");
796 cnv.push_attribute(("id", id_str.as_str()));
797 cnv.push_attribute(("name", name.as_str()));
798 w.write_event(Event::Empty(cnv))?;
799
800 let cnv_pic = BytesStart::new("p:cNvPicPr");
801 w.write_event(Event::Start(cnv_pic))?;
802 w.write_event(Event::Empty(BytesStart::new("a:picLocks")))?;
803 w.write_event(Event::End(BytesEnd::new("p:cNvPicPr")))?;
804
805 w.write_event(Event::Empty(BytesStart::new("p:nvPr")))?;
806 w.write_event(Event::End(BytesEnd::new("p:nvPicPr")))?;
807
808 w.write_event(Event::Start(BytesStart::new("p:blipFill")))?;
810 let mut blip = BytesStart::new("a:blip");
811 blip.push_attribute(("r:embed", rid));
812 w.write_event(Event::Empty(blip))?;
813
814 w.write_event(Event::Start(BytesStart::new("a:stretch")))?;
815 w.write_event(Event::Empty(BytesStart::new("a:fillRect")))?;
816 w.write_event(Event::End(BytesEnd::new("a:stretch")))?;
817 w.write_event(Event::End(BytesEnd::new("p:blipFill")))?;
818
819 w.write_event(Event::Start(BytesStart::new("p:spPr")))?;
821 write_xfrm_positioned(w, 0, 0, cx, cy)?;
822
823 let mut prst = BytesStart::new("a:prstGeom");
824 prst.push_attribute(("prst", "rect"));
825 w.write_event(Event::Start(prst))?;
826 w.write_event(Event::Empty(BytesStart::new("a:avLst")))?;
827 w.write_event(Event::End(BytesEnd::new("a:prstGeom")))?;
828 w.write_event(Event::End(BytesEnd::new("p:spPr")))?;
829
830 w.write_event(Event::End(BytesEnd::new("p:pic")))?;
831 Ok(())
832}
833
834fn empty_with_attrs(
837 w: &mut Writer<&mut Cursor<Vec<u8>>>,
838 tag: &str,
839 attrs: &[(&str, &str)],
840) -> Result<()> {
841 let mut el = BytesStart::new(tag);
842 for (k, v) in attrs {
843 el.push_attribute((*k, *v));
844 }
845 w.write_event(Event::Empty(el))?;
846 Ok(())
847}
848
849fn write_xfrm_group(w: &mut Writer<&mut Cursor<Vec<u8>>>) -> Result<()> {
850 w.write_event(Event::Start(BytesStart::new("a:xfrm")))?;
851
852 empty_with_attrs(w, "a:off", &[("x", "0"), ("y", "0")])?;
853 empty_with_attrs(w, "a:ext", &[("cx", "0"), ("cy", "0")])?;
854 empty_with_attrs(w, "a:chOff", &[("x", "0"), ("y", "0")])?;
855 empty_with_attrs(w, "a:chExt", &[("cx", "0"), ("cy", "0")])?;
856
857 w.write_event(Event::End(BytesEnd::new("a:xfrm")))?;
858 Ok(())
859}
860
861fn write_xfrm_positioned(
862 w: &mut Writer<&mut Cursor<Vec<u8>>>,
863 x: i64,
864 y: i64,
865 cx: i64,
866 cy: i64,
867) -> Result<()> {
868 w.write_event(Event::Start(BytesStart::new("a:xfrm")))?;
869
870 let xs = x.to_string();
871 let ys = y.to_string();
872 let cxs = cx.to_string();
873 let cys = cy.to_string();
874
875 empty_with_attrs(w, "a:off", &[("x", &xs), ("y", &ys)])?;
876 empty_with_attrs(w, "a:ext", &[("cx", &cxs), ("cy", &cys)])?;
877
878 w.write_event(Event::End(BytesEnd::new("a:xfrm")))?;
879 Ok(())
880}
881
882fn image_ext(content_type: &str) -> &str {
883 match content_type {
884 "image/jpeg" => "jpeg",
885 "image/png" => "png",
886 "image/gif" => "gif",
887 "image/tiff" => "tiff",
888 _ => "png",
889 }
890}
891
892pub fn extracted_to_pptx_image(img: &ExtractedImage, idx: usize) -> PptxImage {
894 let (content_type, ext) = match img.filter {
895 ImageFilter::Jpeg => ("image/jpeg", "jpeg"),
896 _ => ("image/png", "png"),
897 };
898
899 PptxImage {
900 data: img.data.clone(),
901 width: img.width,
902 height: img.height,
903 content_type: content_type.to_string(),
904 filename: format!("image{idx}.{ext}"),
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911
912 fn assert_unwind_safe<T: std::panic::UnwindSafe>() {}
915
916 #[test]
917 fn the_writer_stays_unwind_safe() {
918 assert_unwind_safe::<PptxWriter<std::io::Cursor<Vec<u8>>>>();
919 }
920
921 #[test]
922 fn write_minimal_pptx() {
923 let slides = vec![SlideData {
924 text_blocks: vec![TextBlock {
925 text: "Hello World".to_string(),
926 page: 1,
927 bbox: [72.0, 700.0, 200.0, 712.0],
928 font_name: "F1".to_string(),
929 font_size: 12.0,
930 actual_text: None,
931 base_font: None,
932 is_bold: false,
933 is_italic: false,
934 color: None,
935 width_source: Default::default(),
936 char_bounds: vec![],
937 }],
938 images: vec![],
939 page_width: 612.0,
940 page_height: 792.0,
941 }];
942
943 let mut output = Vec::new();
944 write_pptx(&slides, &mut output).unwrap();
945 assert!(output.len() > 100);
946 assert_eq!(&output[0..2], b"PK");
947 }
948
949 #[test]
950 fn write_empty_pptx() {
951 let slides = vec![SlideData {
952 text_blocks: vec![],
953 images: vec![],
954 page_width: 612.0,
955 page_height: 792.0,
956 }];
957
958 let mut output = Vec::new();
959 write_pptx(&slides, &mut output).unwrap();
960 assert!(output.len() > 100);
961 }
962
963 #[test]
964 fn write_multi_slide_pptx() {
965 let slides = vec![
966 SlideData {
967 text_blocks: vec![TextBlock {
968 text: "Slide 1".to_string(),
969 page: 1,
970 bbox: [72.0, 700.0, 200.0, 712.0],
971 font_name: "F1".to_string(),
972 font_size: 24.0,
973 actual_text: None,
974 base_font: None,
975 is_bold: false,
976 is_italic: false,
977 color: None,
978 width_source: Default::default(),
979 char_bounds: vec![],
980 }],
981 images: vec![],
982 page_width: 612.0,
983 page_height: 792.0,
984 },
985 SlideData {
986 text_blocks: vec![TextBlock {
987 text: "Slide 2".to_string(),
988 page: 2,
989 bbox: [72.0, 700.0, 200.0, 712.0],
990 font_name: "F1".to_string(),
991 font_size: 18.0,
992 actual_text: None,
993 base_font: None,
994 is_bold: false,
995 is_italic: false,
996 color: None,
997 width_source: Default::default(),
998 char_bounds: vec![],
999 }],
1000 images: vec![],
1001 page_width: 612.0,
1002 page_height: 792.0,
1003 },
1004 ];
1005
1006 let mut output = Vec::new();
1007 write_pptx(&slides, &mut output).unwrap();
1008 assert!(output.len() > 100);
1009 }
1010}