1pub use crate::generator::{
23 SlideContent, SlideLayout,
24 Shape, ShapeType, ShapeFill, ShapeLine,
25 Image,
26 Connector, ConnectorType, ArrowType,
27 create_pptx, create_pptx_with_content,
28 BulletStyle, BulletPoint,
29 TextFormat, FormattedText,
30};
31
32pub use crate::generator::shapes::{
33 GradientFill, GradientDirection, GradientStop,
34};
35
36pub use crate::elements::{Color, RgbColor, Position, Size};
37pub use crate::core::{Dimension, FlexPosition, FlexSize};
38pub use crate::exc::Result;
39
40pub mod font_sizes {
54 pub const TITLE: u32 = 44;
56 pub const SUBTITLE: u32 = 32;
58 pub const HEADING: u32 = 28;
60 pub const BODY: u32 = 18;
62 pub const SMALL: u32 = 14;
64 pub const CAPTION: u32 = 12;
66 pub const CODE: u32 = 14;
68 pub const LARGE: u32 = 36;
70 pub const XLARGE: u32 = 48;
72
73 pub fn to_emu(pt: u32) -> u32 {
75 pt * 100
76 }
77}
78
79#[macro_export]
81macro_rules! pptx {
82 ($title:expr) => {
83 $crate::prelude::QuickPptx::new($title)
84 };
85}
86
87#[macro_export]
89macro_rules! shape {
90 (rect $x:expr, $y:expr, $w:expr, $h:expr) => {
92 $crate::prelude::Shape::new(
93 $crate::prelude::ShapeType::Rectangle,
94 $crate::prelude::inches($x),
95 $crate::prelude::inches($y),
96 $crate::prelude::inches($w),
97 $crate::prelude::inches($h),
98 )
99 };
100 (circle $x:expr, $y:expr, $size:expr) => {
102 $crate::prelude::Shape::new(
103 $crate::prelude::ShapeType::Circle,
104 $crate::prelude::inches($x),
105 $crate::prelude::inches($y),
106 $crate::prelude::inches($size),
107 $crate::prelude::inches($size),
108 )
109 };
110}
111
112pub fn inches(val: f64) -> u32 {
114 (val * 914400.0) as u32
115}
116
117pub fn cm(val: f64) -> u32 {
119 (val * 360000.0) as u32
120}
121
122pub fn pt(val: f64) -> u32 {
124 (val * 12700.0) as u32
125}
126
127pub struct QuickPptx {
129 title: String,
130 slides: Vec<SlideContent>,
131}
132
133impl QuickPptx {
134 pub fn new(title: &str) -> Self {
136 QuickPptx {
137 title: title.to_string(),
138 slides: Vec::new(),
139 }
140 }
141
142 pub fn slide(mut self, title: &str, bullets: &[&str]) -> Self {
144 let mut slide = SlideContent::new(title);
145 for bullet in bullets {
146 slide = slide.add_bullet(*bullet);
147 }
148 self.slides.push(slide);
149 self
150 }
151
152 pub fn title_slide(mut self, title: &str) -> Self {
154 self.slides.push(SlideContent::new(title));
155 self
156 }
157
158 pub fn content_slide(mut self, slide: SlideContent) -> Self {
160 self.slides.push(slide);
161 self
162 }
163
164 pub fn shapes_slide(mut self, title: &str, shapes: Vec<Shape>) -> Self {
166 let slide = SlideContent::new(title).with_shapes(shapes);
167 self.slides.push(slide);
168 self
169 }
170
171 pub fn build(self) -> std::result::Result<Vec<u8>, Box<dyn std::error::Error>> {
173 if self.slides.is_empty() {
174 create_pptx(&self.title, 1)
176 } else {
177 create_pptx_with_content(&self.title, self.slides)
178 }
179 }
180
181 pub fn save(self, path: &str) -> std::result::Result<(), Box<dyn std::error::Error>> {
183 let data = self.build()?;
184 std::fs::write(path, data)?;
185 Ok(())
186 }
187}
188
189
190pub mod shapes {
192 use super::*;
193
194 pub fn rect(x: f64, y: f64, width: f64, height: f64) -> Shape {
196 Shape::new(ShapeType::Rectangle, inches(x), inches(y), inches(width), inches(height))
197 }
198
199 pub fn rect_emu(x: u32, y: u32, width: u32, height: u32) -> Shape {
201 Shape::new(ShapeType::Rectangle, x, y, width, height)
202 }
203
204 pub fn circle(x: f64, y: f64, diameter: f64) -> Shape {
206 Shape::new(ShapeType::Circle, inches(x), inches(y), inches(diameter), inches(diameter))
207 }
208
209 pub fn circle_emu(x: u32, y: u32, diameter: u32) -> Shape {
211 Shape::new(ShapeType::Circle, x, y, diameter, diameter)
212 }
213
214 pub fn rounded_rect(x: f64, y: f64, width: f64, height: f64) -> Shape {
216 Shape::new(ShapeType::RoundedRectangle, inches(x), inches(y), inches(width), inches(height))
217 }
218
219 pub fn text_box(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
221 Shape::new(ShapeType::Rectangle, inches(x), inches(y), inches(width), inches(height))
222 .with_text(text)
223 }
224
225 pub fn colored(shape: Shape, fill: &str, line: Option<&str>) -> Shape {
227 let mut s = shape.with_fill(ShapeFill::new(fill));
228 if let Some(l) = line {
229 s = s.with_line(ShapeLine::new(l, 12700));
230 }
231 s
232 }
233
234 pub fn gradient(shape: Shape, start: &str, end: &str, direction: GradientDirection) -> Shape {
236 shape.with_gradient(GradientFill::linear(start, end, direction))
237 }
238
239 pub fn arrow_right(x: f64, y: f64, width: f64, height: f64) -> Shape {
241 Shape::new(ShapeType::RightArrow, inches(x), inches(y), inches(width), inches(height))
242 }
243
244 pub fn arrow_left(x: f64, y: f64, width: f64, height: f64) -> Shape {
246 Shape::new(ShapeType::LeftArrow, inches(x), inches(y), inches(width), inches(height))
247 }
248
249 pub fn arrow_up(x: f64, y: f64, width: f64, height: f64) -> Shape {
251 Shape::new(ShapeType::UpArrow, inches(x), inches(y), inches(width), inches(height))
252 }
253
254 pub fn arrow_down(x: f64, y: f64, width: f64, height: f64) -> Shape {
256 Shape::new(ShapeType::DownArrow, inches(x), inches(y), inches(width), inches(height))
257 }
258
259 pub fn diamond(x: f64, y: f64, size: f64) -> Shape {
261 Shape::new(ShapeType::Diamond, inches(x), inches(y), inches(size), inches(size))
262 }
263
264 pub fn triangle(x: f64, y: f64, width: f64, height: f64) -> Shape {
266 Shape::new(ShapeType::Triangle, inches(x), inches(y), inches(width), inches(height))
267 }
268
269 pub fn star(x: f64, y: f64, size: f64) -> Shape {
271 Shape::new(ShapeType::Star5, inches(x), inches(y), inches(size), inches(size))
272 }
273
274 pub fn heart(x: f64, y: f64, size: f64) -> Shape {
276 Shape::new(ShapeType::Heart, inches(x), inches(y), inches(size), inches(size))
277 }
278
279 pub fn cloud(x: f64, y: f64, width: f64, height: f64) -> Shape {
281 Shape::new(ShapeType::Cloud, inches(x), inches(y), inches(width), inches(height))
282 }
283
284 pub fn callout(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
286 Shape::new(ShapeType::WedgeRectCallout, inches(x), inches(y), inches(width), inches(height))
287 .with_text(text)
288 }
289
290 pub fn badge(x: f64, y: f64, text: &str, fill_color: &str) -> Shape {
292 Shape::new(ShapeType::RoundedRectangle, inches(x), inches(y), inches(1.5), inches(0.4))
293 .with_fill(ShapeFill::new(fill_color))
294 .with_text(text)
295 }
296
297 pub fn process(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
299 Shape::new(ShapeType::FlowChartProcess, inches(x), inches(y), inches(width), inches(height))
300 .with_text(text)
301 }
302
303 pub fn decision(x: f64, y: f64, size: f64, text: &str) -> Shape {
305 Shape::new(ShapeType::FlowChartDecision, inches(x), inches(y), inches(size), inches(size))
306 .with_text(text)
307 }
308
309 pub fn document(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
311 Shape::new(ShapeType::FlowChartDocument, inches(x), inches(y), inches(width), inches(height))
312 .with_text(text)
313 }
314
315 pub fn data(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
317 Shape::new(ShapeType::FlowChartData, inches(x), inches(y), inches(width), inches(height))
318 .with_text(text)
319 }
320
321 pub fn terminator(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
323 Shape::new(ShapeType::FlowChartTerminator, inches(x), inches(y), inches(width), inches(height))
324 .with_text(text)
325 }
326
327 pub fn dim(shape_type: ShapeType, x: Dimension, y: Dimension, width: Dimension, height: Dimension) -> Shape {
340 Shape::from_dimensions(shape_type, x, y, width, height)
341 }
342
343 pub fn rect_ratio(x: f64, y: f64, width: f64, height: f64) -> Shape {
352 Shape::from_dimensions(
353 ShapeType::Rectangle,
354 Dimension::Ratio(x), Dimension::Ratio(y),
355 Dimension::Ratio(width), Dimension::Ratio(height),
356 )
357 }
358
359 pub fn text_box_ratio(x: f64, y: f64, width: f64, height: f64, text: &str) -> Shape {
361 rect_ratio(x, y, width, height).with_text(text)
362 }
363}
364
365pub mod colors {
367 pub const RED: &str = "FF0000";
368 pub const GREEN: &str = "00FF00";
369 pub const BLUE: &str = "0000FF";
370 pub const WHITE: &str = "FFFFFF";
371 pub const BLACK: &str = "000000";
372 pub const GRAY: &str = "808080";
373 pub const LIGHT_GRAY: &str = "D3D3D3";
374 pub const DARK_GRAY: &str = "404040";
375 pub const YELLOW: &str = "FFFF00";
376 pub const ORANGE: &str = "FFA500";
377 pub const PURPLE: &str = "800080";
378 pub const CYAN: &str = "00FFFF";
379 pub const MAGENTA: &str = "FF00FF";
380 pub const NAVY: &str = "000080";
381 pub const TEAL: &str = "008080";
382 pub const OLIVE: &str = "808000";
383
384 pub const CORPORATE_BLUE: &str = "1565C0";
386 pub const CORPORATE_GREEN: &str = "2E7D32";
387 pub const CORPORATE_RED: &str = "C62828";
388 pub const CORPORATE_ORANGE: &str = "EF6C00";
389
390 pub const MATERIAL_RED: &str = "F44336";
392 pub const MATERIAL_PINK: &str = "E91E63";
393 pub const MATERIAL_PURPLE: &str = "9C27B0";
394 pub const MATERIAL_INDIGO: &str = "3F51B5";
395 pub const MATERIAL_BLUE: &str = "2196F3";
396 pub const MATERIAL_CYAN: &str = "00BCD4";
397 pub const MATERIAL_TEAL: &str = "009688";
398 pub const MATERIAL_GREEN: &str = "4CAF50";
399 pub const MATERIAL_LIME: &str = "CDDC39";
400 pub const MATERIAL_AMBER: &str = "FFC107";
401 pub const MATERIAL_ORANGE: &str = "FF9800";
402 pub const MATERIAL_BROWN: &str = "795548";
403 pub const MATERIAL_GRAY: &str = "9E9E9E";
404
405 pub const CARBON_BLUE_60: &str = "0043CE";
407 pub const CARBON_BLUE_40: &str = "4589FF";
408 pub const CARBON_GRAY_100: &str = "161616";
409 pub const CARBON_GRAY_80: &str = "393939";
410 pub const CARBON_GRAY_20: &str = "E0E0E0";
411 pub const CARBON_GREEN_50: &str = "24A148";
412 pub const CARBON_RED_60: &str = "DA1E28";
413 pub const CARBON_PURPLE_60: &str = "8A3FFC";
414}
415
416pub mod themes {
418 #[derive(Debug, Clone)]
420 pub struct Theme {
421 pub name: &'static str,
422 pub primary: &'static str,
423 pub secondary: &'static str,
424 pub accent: &'static str,
425 pub background: &'static str,
426 pub text: &'static str,
427 pub light: &'static str,
428 pub dark: &'static str,
429 }
430
431 pub const CORPORATE: Theme = Theme {
433 name: "Corporate",
434 primary: "1565C0",
435 secondary: "1976D2",
436 accent: "FF6F00",
437 background: "FFFFFF",
438 text: "212121",
439 light: "E3F2FD",
440 dark: "0D47A1",
441 };
442
443 pub const MODERN: Theme = Theme {
445 name: "Modern",
446 primary: "212121",
447 secondary: "757575",
448 accent: "00BCD4",
449 background: "FAFAFA",
450 text: "212121",
451 light: "F5F5F5",
452 dark: "424242",
453 };
454
455 pub const VIBRANT: Theme = Theme {
457 name: "Vibrant",
458 primary: "E91E63",
459 secondary: "9C27B0",
460 accent: "FF9800",
461 background: "FFFFFF",
462 text: "212121",
463 light: "FCE4EC",
464 dark: "880E4F",
465 };
466
467 pub const DARK: Theme = Theme {
469 name: "Dark",
470 primary: "BB86FC",
471 secondary: "03DAC6",
472 accent: "CF6679",
473 background: "121212",
474 text: "FFFFFF",
475 light: "1E1E1E",
476 dark: "000000",
477 };
478
479 pub const NATURE: Theme = Theme {
481 name: "Nature",
482 primary: "2E7D32",
483 secondary: "4CAF50",
484 accent: "8BC34A",
485 background: "FFFFFF",
486 text: "1B5E20",
487 light: "E8F5E9",
488 dark: "1B5E20",
489 };
490
491 pub const TECH: Theme = Theme {
493 name: "Tech",
494 primary: "0D47A1",
495 secondary: "1976D2",
496 accent: "00E676",
497 background: "FAFAFA",
498 text: "263238",
499 light: "E3F2FD",
500 dark: "01579B",
501 };
502
503 pub const CARBON: Theme = Theme {
505 name: "Carbon",
506 primary: "0043CE",
507 secondary: "4589FF",
508 accent: "24A148",
509 background: "FFFFFF",
510 text: "161616",
511 light: "E0E0E0",
512 dark: "161616",
513 };
514
515 pub fn all() -> Vec<Theme> {
517 vec![CORPORATE, MODERN, VIBRANT, DARK, NATURE, TECH, CARBON]
518 }
519}
520
521pub mod layouts {
523 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 {
534 (SLIDE_WIDTH - shape_width) / 2
535 }
536
537 pub fn center_y(shape_height: u32) -> u32 {
539 (SLIDE_HEIGHT - shape_height) / 2
540 }
541
542 pub fn center(shape_width: u32, shape_height: u32) -> (u32, u32) {
544 (center_x(shape_width), center_y(shape_height))
545 }
546
547 pub fn grid(rows: usize, cols: usize, cell_width: u32, cell_height: u32) -> Vec<(u32, u32)> {
550 let mut positions = Vec::new();
551 let total_width = cell_width * cols as u32;
552 let total_height = cell_height * rows as u32;
553 let start_x = center_x(total_width);
554 let start_y = center_y(total_height);
555
556 for row in 0..rows {
557 for col in 0..cols {
558 let x = start_x + (col as u32 * cell_width);
559 let y = start_y + (row as u32 * cell_height);
560 positions.push((x, y));
561 }
562 }
563 positions
564 }
565
566 pub fn stack_horizontal(count: usize, shape_width: u32, spacing: u32, y: u32) -> Vec<(u32, u32)> {
568 let total_width = (shape_width * count as u32) + (spacing * (count - 1) as u32);
569 let start_x = center_x(total_width);
570
571 (0..count)
572 .map(|i| (start_x + (i as u32 * (shape_width + spacing)), y))
573 .collect()
574 }
575
576 pub fn stack_vertical(count: usize, shape_height: u32, spacing: u32, x: u32) -> Vec<(u32, u32)> {
578 let total_height = (shape_height * count as u32) + (spacing * (count - 1) as u32);
579 let start_y = center_y(total_height);
580
581 (0..count)
582 .map(|i| (x, start_y + (i as u32 * (shape_height + spacing))))
583 .collect()
584 }
585
586 pub fn distribute_horizontal(count: usize, shape_width: u32, y: u32) -> Vec<(u32, u32)> {
588 if count == 0 {
589 return vec![];
590 }
591 if count == 1 {
592 return vec![(center_x(shape_width), y)];
593 }
594
595 let usable_width = SLIDE_WIDTH - (2 * MARGIN);
596 let spacing = (usable_width - (shape_width * count as u32)) / (count as u32 - 1);
597
598 (0..count)
599 .map(|i| (MARGIN + (i as u32 * (shape_width + spacing)), y))
600 .collect()
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn test_quick_pptx() {
610 let result = QuickPptx::new("Test")
611 .slide("Slide 1", &["Point 1", "Point 2"])
612 .build();
613 assert!(result.is_ok());
614 }
615
616 #[test]
617 fn test_inches_conversion() {
618 assert_eq!(inches(1.0), 914400);
619 assert_eq!(cm(2.54), 914400); }
621
622 #[test]
623 fn test_shape_builders() {
624 let rect = shapes::rect(1.0, 1.0, 2.0, 1.0);
625 assert_eq!(rect.width, inches(2.0));
626
627 let circle = shapes::circle(1.0, 1.0, 1.0);
628 assert_eq!(circle.width, circle.height);
629 }
630
631 #[test]
632 fn test_arrow_shapes() {
633 let arrow = shapes::arrow_right(1.0, 1.0, 2.0, 1.0);
634 assert_eq!(arrow.width, inches(2.0));
635
636 let up = shapes::arrow_up(1.0, 1.0, 1.0, 2.0);
637 assert_eq!(up.height, inches(2.0));
638 }
639
640 #[test]
641 fn test_flowchart_shapes() {
642 let process = shapes::process(1.0, 1.0, 2.0, 1.0, "Process");
643 assert!(process.text.is_some());
644
645 let decision = shapes::decision(1.0, 1.0, 1.5, "Yes/No");
646 assert!(decision.text.is_some());
647 }
648
649 #[test]
650 fn test_badge_shape() {
651 let badge = shapes::badge(1.0, 1.0, "NEW", colors::MATERIAL_GREEN);
652 assert!(badge.text.is_some());
653 assert!(badge.fill.is_some());
654 }
655
656 #[test]
657 fn test_themes() {
658 let all_themes = crate::prelude::themes::all();
659 assert_eq!(all_themes.len(), 7);
660
661 assert_eq!(crate::prelude::themes::CORPORATE.name, "Corporate");
662 assert_eq!(crate::prelude::themes::DARK.background, "121212");
663 }
664
665 #[test]
666 fn test_layouts_center() {
667 let (x, y) = crate::prelude::layouts::center(1000000, 500000);
668 assert!(x > 0);
669 assert!(y > 0);
670
671 assert_eq!(x, (crate::prelude::layouts::SLIDE_WIDTH - 1000000) / 2);
673 assert_eq!(y, (crate::prelude::layouts::SLIDE_HEIGHT - 500000) / 2);
674 }
675
676 #[test]
677 fn test_layouts_grid() {
678 let positions = crate::prelude::layouts::grid(2, 3, 1000000, 800000);
679 assert_eq!(positions.len(), 6);
680 }
681
682 #[test]
683 fn test_layouts_stack_horizontal() {
684 let positions = crate::prelude::layouts::stack_horizontal(4, 500000, 100000, 2000000);
685 assert_eq!(positions.len(), 4);
686
687 for i in 1..positions.len() {
689 let diff = positions[i].0 - positions[i-1].0;
690 assert_eq!(diff, 600000); }
692 }
693
694 #[test]
695 fn test_layouts_distribute_horizontal() {
696 let positions = crate::prelude::layouts::distribute_horizontal(3, 500000, 2000000);
697 assert_eq!(positions.len(), 3);
698 }
699
700 #[test]
701 fn test_material_colors() {
702 assert_eq!(colors::MATERIAL_RED, "F44336");
703 assert_eq!(colors::MATERIAL_BLUE, "2196F3");
704 assert_eq!(colors::MATERIAL_GREEN, "4CAF50");
705 }
706
707 #[test]
708 fn test_carbon_colors() {
709 assert_eq!(colors::CARBON_BLUE_60, "0043CE");
710 assert_eq!(colors::CARBON_GRAY_100, "161616");
711 }
712}