1pub use crate::generator::{
23 create_pptx, create_pptx_with_content, ArrowType, BulletPoint, BulletStyle, Connector,
24 ConnectorType, FormattedText, Image, Shape, ShapeFill, ShapeLine, ShapeType, SlideContent,
25 SlideLayout, TextFormat,
26};
27
28pub use crate::generator::shapes::{GradientDirection, GradientFill, GradientStop};
29
30pub use crate::core::{Dimension, FlexPosition, FlexSize};
31pub use crate::elements::{Color, Position, RgbColor, Size};
32pub use crate::exc::Result;
33
34pub use crate::helpers::{
36 black,
37 blue,
38 brown,
39 cell,
40 circle,
41 corporate_blue,
42 corporate_green,
43 cyan,
44 diamond,
45 ellipse,
46 gray,
47 green,
48 header_cell,
49 hex,
50 highlight_cell,
51 image,
52 image_file,
53 magenta,
54 material_blue,
55 material_green,
56 material_orange,
57 material_red,
58 orange,
59 pink,
60 purple,
61 rect,
62 red,
63 rgb,
64 rounded_rect,
65 simple_table,
67 table_from_data,
68 table_with_header,
69 table_with_widths,
70 triangle,
71 white,
72 yellow,
73 ColorValue,
75 QuickTable,
76 ShapeExt,
77};
78
79pub mod font_sizes {
93 pub const TITLE: u32 = 44;
95 pub const SUBTITLE: u32 = 32;
97 pub const HEADING: u32 = 28;
99 pub const BODY: u32 = 18;
101 pub const SMALL: u32 = 14;
103 pub const CAPTION: u32 = 12;
105 pub const CODE: u32 = 14;
107 pub const LARGE: u32 = 36;
109 pub const XLARGE: u32 = 48;
111
112 pub fn to_emu(pt: u32) -> u32 {
114 pt * 100
115 }
116}
117
118#[macro_export]
120macro_rules! pptx {
121 ($title:expr) => {
122 $crate::prelude::QuickPptx::new($title)
123 };
124}
125
126#[macro_export]
128macro_rules! shape {
129 (rect $x:expr, $y:expr, $w:expr, $h:expr) => {
131 $crate::prelude::Shape::new(
132 $crate::prelude::ShapeType::Rectangle,
133 $crate::prelude::inches($x),
134 $crate::prelude::inches($y),
135 $crate::prelude::inches($w),
136 $crate::prelude::inches($h),
137 )
138 };
139 (circle $x:expr, $y:expr, $size:expr) => {
141 $crate::prelude::Shape::new(
142 $crate::prelude::ShapeType::Circle,
143 $crate::prelude::inches($x),
144 $crate::prelude::inches($y),
145 $crate::prelude::inches($size),
146 $crate::prelude::inches($size),
147 )
148 };
149}
150
151pub fn inches(val: f64) -> u32 {
153 (val * 914400.0) as u32
154}
155
156pub fn cm(val: f64) -> u32 {
158 (val * 360000.0) as u32
159}
160
161pub fn pt(val: f64) -> u32 {
163 (val * 12700.0) as u32
164}
165
166pub struct QuickPptx {
168 title: String,
169 slides: Vec<SlideContent>,
170}
171
172impl QuickPptx {
173 pub fn new(title: &str) -> Self {
175 QuickPptx {
176 title: title.to_string(),
177 slides: Vec::new(),
178 }
179 }
180
181 pub fn slide(mut self, title: &str, bullets: &[&str]) -> Self {
183 let mut slide = SlideContent::new(title);
184 for bullet in bullets {
185 slide = slide.add_bullet(*bullet);
186 }
187 self.slides.push(slide);
188 self
189 }
190
191 pub fn title_slide(mut self, title: &str) -> Self {
193 self.slides.push(SlideContent::new(title));
194 self
195 }
196
197 pub fn content_slide(mut self, slide: SlideContent) -> Self {
199 self.slides.push(slide);
200 self
201 }
202
203 pub fn shapes_slide(mut self, title: &str, shapes: Vec<Shape>) -> Self {
205 let slide = SlideContent::new(title).with_shapes(shapes);
206 self.slides.push(slide);
207 self
208 }
209
210 pub fn build(self) -> crate::exc::Result<Vec<u8>> {
212 if self.slides.is_empty() {
213 create_pptx(&self.title, 1)
215 } else {
216 create_pptx_with_content(&self.title, self.slides)
217 }
218 }
219
220 pub fn save(self, path: &str) -> crate::exc::Result<()> {
222 let data = self.build()?;
223 std::fs::write(path, data)?;
224 Ok(())
225 }
226}
227
228pub mod shapes {
230 use super::*;
231
232 pub fn rect(x: f64, y: f64, width: f64, height: f64) -> Shape {
234 Shape::new(
235 ShapeType::Rectangle,
236 inches(x),
237 inches(y),
238 inches(width),
239 inches(height),
240 )
241 }
242
243 pub fn rect_emu(x: u32, y: u32, width: u32, height: u32) -> Shape {
245 Shape::new(ShapeType::Rectangle, x, y, width, height)
246 }
247
248 pub fn circle(x: f64, y: f64, diameter: f64) -> Shape {
250 Shape::new(
251 ShapeType::Circle,
252 inches(x),
253 inches(y),
254 inches(diameter),
255 inches(diameter),
256 )
257 }
258
259 pub fn circle_emu(x: u32, y: u32, diameter: u32) -> Shape {
261 Shape::new(ShapeType::Circle, x, y, diameter, diameter)
262 }
263
264 pub fn rounded_rect(x: f64, y: f64, width: f64, height: f64) -> Shape {
266 Shape::new(
267 ShapeType::RoundedRectangle,
268 inches(x),
269 inches(y),
270 inches(width),
271 inches(height),
272 )
273 }
274
275 pub fn text_box(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
277 Shape::new(
278 ShapeType::Rectangle,
279 inches(x),
280 inches(y),
281 inches(width),
282 inches(height),
283 )
284 .with_text(text)
285 }
286
287 pub fn colored(shape: Shape, fill: &str, line: Option<&str>) -> Shape {
289 let mut s = shape.with_fill(ShapeFill::new(fill));
290 if let Some(l) = line {
291 s = s.with_line(ShapeLine::new(l, 12700));
292 }
293 s
294 }
295
296 pub fn gradient(shape: Shape, start: &str, end: &str, direction: GradientDirection) -> Shape {
298 shape.with_gradient(GradientFill::linear(start, end, direction))
299 }
300
301 pub fn arrow_right(x: f64, y: f64, width: f64, height: f64) -> Shape {
303 Shape::new(
304 ShapeType::RightArrow,
305 inches(x),
306 inches(y),
307 inches(width),
308 inches(height),
309 )
310 }
311
312 pub fn arrow_left(x: f64, y: f64, width: f64, height: f64) -> Shape {
314 Shape::new(
315 ShapeType::LeftArrow,
316 inches(x),
317 inches(y),
318 inches(width),
319 inches(height),
320 )
321 }
322
323 pub fn arrow_up(x: f64, y: f64, width: f64, height: f64) -> Shape {
325 Shape::new(
326 ShapeType::UpArrow,
327 inches(x),
328 inches(y),
329 inches(width),
330 inches(height),
331 )
332 }
333
334 pub fn arrow_down(x: f64, y: f64, width: f64, height: f64) -> Shape {
336 Shape::new(
337 ShapeType::DownArrow,
338 inches(x),
339 inches(y),
340 inches(width),
341 inches(height),
342 )
343 }
344
345 pub fn diamond(x: f64, y: f64, size: f64) -> Shape {
347 Shape::new(
348 ShapeType::Diamond,
349 inches(x),
350 inches(y),
351 inches(size),
352 inches(size),
353 )
354 }
355
356 pub fn triangle(x: f64, y: f64, width: f64, height: f64) -> Shape {
358 Shape::new(
359 ShapeType::Triangle,
360 inches(x),
361 inches(y),
362 inches(width),
363 inches(height),
364 )
365 }
366
367 pub fn star(x: f64, y: f64, size: f64) -> Shape {
369 Shape::new(
370 ShapeType::Star5,
371 inches(x),
372 inches(y),
373 inches(size),
374 inches(size),
375 )
376 }
377
378 pub fn heart(x: f64, y: f64, size: f64) -> Shape {
380 Shape::new(
381 ShapeType::Heart,
382 inches(x),
383 inches(y),
384 inches(size),
385 inches(size),
386 )
387 }
388
389 pub fn cloud(x: f64, y: f64, width: f64, height: f64) -> Shape {
391 Shape::new(
392 ShapeType::Cloud,
393 inches(x),
394 inches(y),
395 inches(width),
396 inches(height),
397 )
398 }
399
400 pub fn callout(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
402 Shape::new(
403 ShapeType::WedgeRectCallout,
404 inches(x),
405 inches(y),
406 inches(width),
407 inches(height),
408 )
409 .with_text(text)
410 }
411
412 pub fn badge(x: f64, y: f64, text: &str, fill_color: &str) -> Shape {
414 Shape::new(
415 ShapeType::RoundedRectangle,
416 inches(x),
417 inches(y),
418 inches(1.5),
419 inches(0.4),
420 )
421 .with_fill(ShapeFill::new(fill_color))
422 .with_text(text)
423 }
424
425 pub fn process(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
427 Shape::new(
428 ShapeType::FlowChartProcess,
429 inches(x),
430 inches(y),
431 inches(width),
432 inches(height),
433 )
434 .with_text(text)
435 }
436
437 pub fn decision(x: f64, y: f64, size: f64, text: &str) -> Shape {
439 Shape::new(
440 ShapeType::FlowChartDecision,
441 inches(x),
442 inches(y),
443 inches(size),
444 inches(size),
445 )
446 .with_text(text)
447 }
448
449 pub fn document(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
451 Shape::new(
452 ShapeType::FlowChartDocument,
453 inches(x),
454 inches(y),
455 inches(width),
456 inches(height),
457 )
458 .with_text(text)
459 }
460
461 pub fn data(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
463 Shape::new(
464 ShapeType::FlowChartData,
465 inches(x),
466 inches(y),
467 inches(width),
468 inches(height),
469 )
470 .with_text(text)
471 }
472
473 pub fn terminator(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
475 Shape::new(
476 ShapeType::FlowChartTerminator,
477 inches(x),
478 inches(y),
479 inches(width),
480 inches(height),
481 )
482 .with_text(text)
483 }
484
485 pub fn dim(
498 shape_type: ShapeType,
499 x: Dimension,
500 y: Dimension,
501 width: Dimension,
502 height: Dimension,
503 ) -> Shape {
504 Shape::from_dimensions(shape_type, x, y, width, height)
505 }
506
507 pub fn rect_ratio(x: f64, y: f64, width: f64, height: f64) -> Shape {
516 Shape::from_dimensions(
517 ShapeType::Rectangle,
518 Dimension::Ratio(x),
519 Dimension::Ratio(y),
520 Dimension::Ratio(width),
521 Dimension::Ratio(height),
522 )
523 }
524
525 pub fn text_box_ratio(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
527 rect_ratio(x, y, width, height).with_text(text)
528 }
529}
530
531pub mod colors {
533 pub const RED: &str = "FF0000";
534 pub const GREEN: &str = "00FF00";
535 pub const BLUE: &str = "0000FF";
536 pub const WHITE: &str = "FFFFFF";
537 pub const BLACK: &str = "000000";
538 pub const GRAY: &str = "808080";
539 pub const YELLOW: &str = "FFFF00";
540 pub const ORANGE: &str = "FFA500";
541 pub const PURPLE: &str = "800080";
542 pub const CYAN: &str = "00FFFF";
543 pub const MAGENTA: &str = "FF00FF";
544
545 pub const CORPORATE_BLUE: &str = "1565C0";
546 pub const CORPORATE_GREEN: &str = "2E7D32";
547 pub const CORPORATE_ORANGE: &str = "EF6C00";
548
549 pub const MATERIAL_RED: &str = "F44336";
550 pub const MATERIAL_PINK: &str = "E91E63";
551 pub const MATERIAL_PURPLE: &str = "9C27B0";
552 pub const MATERIAL_INDIGO: &str = "3F51B5";
553 pub const MATERIAL_BLUE: &str = "2196F3";
554 pub const MATERIAL_CYAN: &str = "00BCD4";
555 pub const MATERIAL_TEAL: &str = "009688";
556 pub const MATERIAL_GREEN: &str = "4CAF50";
557 pub const MATERIAL_LIME: &str = "CDDC39";
558 pub const MATERIAL_AMBER: &str = "FFC107";
559 pub const MATERIAL_ORANGE: &str = "FF9800";
560 pub const MATERIAL_BROWN: &str = "795548";
561 pub const MATERIAL_GRAY: &str = "9E9E9E";
562
563 pub const CARBON_BLUE_60: &str = "0043CE";
564 pub const CARBON_BLUE_40: &str = "4589FF";
565 pub const CARBON_GRAY_100: &str = "161616";
566 pub const CARBON_GRAY_80: &str = "393939";
567 pub const CARBON_GRAY_20: &str = "E0E0E0";
568 pub const CARBON_GREEN_50: &str = "24A148";
569 pub const CARBON_RED_60: &str = "DA1E28";
570 pub const CARBON_PURPLE_60: &str = "8A3FFC";
571}
572
573pub mod themes {
575 #[derive(Debug, Clone)]
577 pub struct Theme {
578 pub name: &'static str,
579 pub primary: &'static str,
580 pub secondary: &'static str,
581 pub accent: &'static str,
582 pub background: &'static str,
583 pub text: &'static str,
584 pub light: &'static str,
585 pub dark: &'static str,
586 }
587
588 pub const CORPORATE: Theme = Theme {
590 name: "Corporate",
591 primary: "1565C0",
592 secondary: "1976D2",
593 accent: "FF6F00",
594 background: "FFFFFF",
595 text: "212121",
596 light: "E3F2FD",
597 dark: "0D47A1",
598 };
599
600 pub const MODERN: Theme = Theme {
602 name: "Modern",
603 primary: "212121",
604 secondary: "757575",
605 accent: "00BCD4",
606 background: "FAFAFA",
607 text: "212121",
608 light: "F5F5F5",
609 dark: "424242",
610 };
611
612 pub const VIBRANT: Theme = Theme {
614 name: "Vibrant",
615 primary: "E91E63",
616 secondary: "9C27B0",
617 accent: "FF9800",
618 background: "FFFFFF",
619 text: "212121",
620 light: "FCE4EC",
621 dark: "880E4F",
622 };
623
624 pub const DARK: Theme = Theme {
626 name: "Dark",
627 primary: "BB86FC",
628 secondary: "03DAC6",
629 accent: "CF6679",
630 background: "121212",
631 text: "FFFFFF",
632 light: "1E1E1E",
633 dark: "000000",
634 };
635
636 pub const NATURE: Theme = Theme {
638 name: "Nature",
639 primary: "2E7D32",
640 secondary: "4CAF50",
641 accent: "8BC34A",
642 background: "FFFFFF",
643 text: "1B5E20",
644 light: "E8F5E9",
645 dark: "1B5E20",
646 };
647
648 pub const TECH: Theme = Theme {
650 name: "Tech",
651 primary: "0D47A1",
652 secondary: "1976D2",
653 accent: "00E676",
654 background: "FAFAFA",
655 text: "263238",
656 light: "E3F2FD",
657 dark: "01579B",
658 };
659
660 pub const CARBON: Theme = Theme {
662 name: "Carbon",
663 primary: "0043CE",
664 secondary: "4589FF",
665 accent: "24A148",
666 background: "FFFFFF",
667 text: "161616",
668 light: "E0E0E0",
669 dark: "161616",
670 };
671
672 pub fn all() -> Vec<Theme> {
674 vec![CORPORATE, MODERN, VIBRANT, DARK, NATURE, TECH, CARBON]
675 }
676}
677
678pub mod layouts {
680 pub const SLIDE_WIDTH: u32 = 9144000; pub const SLIDE_HEIGHT: u32 = 6858000; pub const MARGIN: u32 = 457200; pub const MARGIN_SMALL: u32 = 228600; pub const MARGIN_LARGE: u32 = 914400; pub fn center_x(shape_width: u32) -> u32 {
691 (SLIDE_WIDTH - shape_width) / 2
692 }
693
694 pub fn center_y(shape_height: u32) -> u32 {
696 (SLIDE_HEIGHT - shape_height) / 2
697 }
698
699 pub fn center(shape_width: u32, shape_height: u32) -> (u32, u32) {
701 (center_x(shape_width), center_y(shape_height))
702 }
703
704 pub fn grid(rows: usize, cols: usize, cell_width: u32, cell_height: u32) -> Vec<(u32, u32)> {
707 let mut positions = Vec::new();
708 let total_width = cell_width * cols as u32;
709 let total_height = cell_height * rows as u32;
710 let start_x = center_x(total_width);
711 let start_y = center_y(total_height);
712
713 for row in 0..rows {
714 for col in 0..cols {
715 let x = start_x + (col as u32 * cell_width);
716 let y = start_y + (row as u32 * cell_height);
717 positions.push((x, y));
718 }
719 }
720 positions
721 }
722
723 pub fn stack_horizontal(
725 count: usize,
726 shape_width: u32,
727 spacing: u32,
728 y: u32,
729 ) -> Vec<(u32, u32)> {
730 let total_width = (shape_width * count as u32) + (spacing * (count - 1) as u32);
731 let start_x = center_x(total_width);
732
733 (0..count)
734 .map(|i| (start_x + (i as u32 * (shape_width + spacing)), y))
735 .collect()
736 }
737
738 pub fn stack_vertical(
740 count: usize,
741 shape_height: u32,
742 spacing: u32,
743 x: u32,
744 ) -> Vec<(u32, u32)> {
745 let total_height = (shape_height * count as u32) + (spacing * (count - 1) as u32);
746 let start_y = center_y(total_height);
747
748 (0..count)
749 .map(|i| (x, start_y + (i as u32 * (shape_height + spacing))))
750 .collect()
751 }
752
753 pub fn distribute_horizontal(count: usize, shape_width: u32, y: u32) -> Vec<(u32, u32)> {
755 if count == 0 {
756 return vec![];
757 }
758 if count == 1 {
759 return vec![(center_x(shape_width), y)];
760 }
761
762 let usable_width = SLIDE_WIDTH - (2 * MARGIN);
763 let spacing = (usable_width - (shape_width * count as u32)) / (count as u32 - 1);
764
765 (0..count)
766 .map(|i| (MARGIN + (i as u32 * (shape_width + spacing)), y))
767 .collect()
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774
775 #[test]
776 fn test_quick_pptx() {
777 let result = QuickPptx::new("Test")
778 .slide("Slide 1", &["Point 1", "Point 2"])
779 .build();
780 assert!(result.is_ok());
781 }
782
783 #[test]
784 fn test_inches_conversion() {
785 assert_eq!(inches(1.0), 914400);
786 assert_eq!(cm(2.54), 914400); }
788
789 #[test]
790 fn test_shape_builders() {
791 let rect = shapes::rect(1.0, 1.0, 2.0, 1.0);
792 assert_eq!(rect.width, inches(2.0));
793
794 let circle = shapes::circle(1.0, 1.0, 1.0);
795 assert_eq!(circle.width, circle.height);
796 }
797
798 #[test]
799 fn test_arrow_shapes() {
800 let arrow = shapes::arrow_right(1.0, 1.0, 2.0, 1.0);
801 assert_eq!(arrow.width, inches(2.0));
802
803 let up = shapes::arrow_up(1.0, 1.0, 1.0, 2.0);
804 assert_eq!(up.height, inches(2.0));
805 }
806
807 #[test]
808 fn test_flowchart_shapes() {
809 let process = shapes::process(1.0, 1.0, 2.0, 1.0, "Process");
810 assert!(process.text.is_some());
811
812 let decision = shapes::decision(1.0, 1.0, 1.5, "Yes/No");
813 assert!(decision.text.is_some());
814 }
815
816 #[test]
817 fn test_badge_shape() {
818 let badge = shapes::badge(1.0, 1.0, "NEW", colors::MATERIAL_GREEN);
819 assert!(badge.text.is_some());
820 assert!(badge.fill.is_some());
821 }
822
823 #[test]
824 fn test_themes() {
825 let all_themes = crate::prelude::themes::all();
826 assert_eq!(all_themes.len(), 7);
827
828 assert_eq!(crate::prelude::themes::CORPORATE.name, "Corporate");
829 assert_eq!(crate::prelude::themes::DARK.background, "121212");
830 }
831
832 #[test]
833 fn test_layouts_center() {
834 let (x, y) = crate::prelude::layouts::center(1000000, 500000);
835 assert!(x > 0);
836 assert!(y > 0);
837
838 assert_eq!(x, (crate::prelude::layouts::SLIDE_WIDTH - 1000000) / 2);
840 assert_eq!(y, (crate::prelude::layouts::SLIDE_HEIGHT - 500000) / 2);
841 }
842
843 #[test]
844 fn test_layouts_grid() {
845 let positions = crate::prelude::layouts::grid(2, 3, 1000000, 800000);
846 assert_eq!(positions.len(), 6);
847 }
848
849 #[test]
850 fn test_layouts_stack_horizontal() {
851 let positions = crate::prelude::layouts::stack_horizontal(4, 500000, 100000, 2000000);
852 assert_eq!(positions.len(), 4);
853
854 for i in 1..positions.len() {
856 let diff = positions[i].0 - positions[i - 1].0;
857 assert_eq!(diff, 600000); }
859 }
860
861 #[test]
862 fn test_layouts_distribute_horizontal() {
863 let positions = crate::prelude::layouts::distribute_horizontal(3, 500000, 2000000);
864 assert_eq!(positions.len(), 3);
865 }
866
867 #[test]
868 fn test_material_colors() {
869 assert_eq!(colors::MATERIAL_RED, "F44336");
870 assert_eq!(colors::MATERIAL_BLUE, "2196F3");
871 assert_eq!(colors::MATERIAL_GREEN, "4CAF50");
872 }
873
874 #[test]
875 fn test_carbon_colors() {
876 assert_eq!(colors::CARBON_BLUE_60, "0043CE");
877 assert_eq!(colors::CARBON_GRAY_100, "161616");
878 }
879}