File size: 7,851 Bytes
2dd1349
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from pptx import Presentation
from pptx.util import Inches as _Inches, Pt as _Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_AUTO_SIZE
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_SHAPE_TYPE
from io import BytesIO

ARROW_ADD = '"""<a:tailEnd type="arrow" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>"""'

class SlideAgent:
    def __init__(self, slide_width=13.33, slide_height=7.5):
        """Initialize a new presentation with specified slide dimensions in inches."""
        self.prs = Presentation()
        self.prs.slide_width = self._inches(slide_width)
        self.prs.slide_height = self._inches(slide_height)
        self.slide = None

    def _inches(self, val):
        """Helper method to convert to Inches."""
        return _Inches(val)

    def _points(self, val):
        """Helper method to convert to Points."""
        return _Pt(val)

    # ------- Slide APIs -------
    def add_slide(self, layout=0):
        """Create a new slide with a specific layout."""
        slide_layout = self.prs.slide_layouts[layout]
        self.slide = self.prs.slides.add_slide(slide_layout)

    # ------- Text APIs -------
    def add_title(self, text, font_size=44, font_color=(0, 0, 0)):
        """Add a title to the slide with a custom font size (in points) and font color (RGB tuple)."""
        title_shape = self.slide.shapes.title
        title_shape.text = text
        self._format_text(title_shape.text_frame, self._points(font_size), RGBColor(*font_color))

    def add_text(self, text, top, left, width, height, font_size=20, bold=False, color=(0, 0, 0), background_color=None, auto_size=True):
        """Add a text box at a specified location with custom text settings and optional background color."""
        # Create the text box shape
        text_box = self.slide.shapes.add_textbox(self._inches(left), self._inches(top), self._inches(width), self._inches(height))

        # Set background color if provided
        if background_color:
            text_box.fill.solid()
            text_box.fill.fore_color.rgb = RGBColor(*background_color)
        else:
            text_box.fill.background()  # No fill if no color is specified

        # Handle line breaks and adjust height
        lines = text.split("\n")
        adjusted_height = height * len(lines)  # Adjust height based on the number of lines
        text_box.height = self._inches(adjusted_height)

        # Set text and format it
        text_frame = text_box.text_frame
        text_frame.word_wrap = True
        if auto_size:
            text_frame.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT  # Automatically fit the text box to the text
        self._format_paragraph(text_frame, text, self._points(font_size), bold, RGBColor(*color))

    def add_bullet_points(self, bullet_points, top, left, width, height, font_size=18, color=(0, 0, 0)):
        """Add a text box with bullet points."""
        text_box = self.slide.shapes.add_textbox(self._inches(left), self._inches(top), self._inches(width), self._inches(height))
        text_frame = text_box.text_frame
        text_frame.word_wrap = True
        text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE

        for point in bullet_points:
            p = text_frame.add_paragraph()
            p.text = point
            self._format_text(p, self._points(font_size), RGBColor(*color))
            p.level = bullet_points.index(point)

    # ------- Image APIs -------
    def add_image(self, image_path, top, left, width, height):
        """Add an image at a specified location."""
        self.slide.shapes.add_picture(image_path, self._inches(left), self._inches(top), self._inches(width), self._inches(height))

    def add_image_centered(self, image_path, image_width, image_height):
        """Add an image centered on the slide."""
        slide_width = self.prs.slide_width.inches
        slide_height = self.prs.slide_height.inches
        left = (slide_width - image_width) / 2
        top = (slide_height - image_height) / 2
        self.add_image(image_path, top, left, image_width, image_height)
        
    # ------- Shape APIs -------
    def add_shape(self, shape_type, top, left, width, height, fill_color=None):
        """Add a shape to the slide, supporting MSO_AUTO_SHAPE_TYPE."""
        if isinstance(shape_type, str):
            # Check if the shape type is a valid string, otherwise raise an error
            try:
                shape_type = getattr(MSO_AUTO_SHAPE_TYPE, shape_type.upper())
            except AttributeError:
                raise ValueError(f"Invalid shape type: {shape_type}. Must be a valid MSO_AUTO_SHAPE_TYPE.")
        
        # Now create the shape with the validated or passed enum type
        shape = self.slide.shapes.add_shape(shape_type, self._inches(left), self._inches(top), self._inches(width), self._inches(height))
        
        if fill_color:
            shape.fill.solid()
            shape.fill.fore_color.rgb = RGBColor(*fill_color)
            
    def add_straight_arrow(self, start_x, start_y, end_x, end_y):
        connector = self.slide.shapes.add_connector("MSO_CONNECTOR.STRAIGHT", start_x, start_y, end_x, end_y)
        
    
    def add_straight_line(self, start_x, start_y, end_x, end_y):
        connector = self.slide.shapes.add_connector("MSO_CONNECTOR.STRAIGHT", start_x, start_y, end_x, end_y)
        line_elem = connector.line._get_or_add_ln()
        line_elem.append(parse_xml({ARROW_ADD}))

    # ------- Table APIs -------
    def add_table(self, rows, cols, top, left, width, height, column_widths=None):
        """Add a table to the slide."""
        table = self.slide.shapes.add_table(rows, cols, left, top, width, height).table
        if column_widths:
            for idx, col_width in enumerate(column_widths):
                table.columns[idx].width = Inches(col_width)
        return table

    # ------- Helper APIs -------
    def set_background_color(self, color):
        """Set background color for the current slide."""
        background = self.slide.background
        fill = background.fill
        fill.solid()
        fill.fore_color.rgb = color

    def duplicate_slide(self, slide_index):
        """Duplicate a slide by index."""
        template_slide = self.prs.slides[slide_index]
        new_slide = self.prs.slides.add_slide(template_slide.slide_layout)
        for shape in template_slide.shapes:
            self._copy_shape(shape, new_slide)

    def save_presentation(self, file_name):
        """Save the PowerPoint presentation."""
        self.prs.save(file_name)

    # ------- Internal Helper Methods -------
    def _format_paragraph(self, text_frame, text, font_size, bold, color):
        """Helper function to format text within a text frame."""
        p = text_frame.add_paragraph()
        p.text = text
        p.font.size = font_size
        p.font.bold = bold
        p.font.color.rgb = color

    def _format_text(self, text_frame, font_size, font_color):
        """Helper function to format text in a text frame."""
        for paragraph in text_frame.paragraphs:
            paragraph.font.size = font_size
            paragraph.font.color.rgb = font_color

    def _copy_shape(self, shape, slide):
        """Copy a shape from one slide to another."""
        if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
            image = BytesIO(shape.image.blob)
            slide.shapes.add_picture(image, shape.left, shape.top, shape.width, shape.height)
        elif shape.has_text_frame:
            new_shape = slide.shapes.add_textbox(shape.left, shape.top, shape.width, shape.height)
            new_shape.text = shape.text
            self._format_text(new_shape.text_frame, shape.text_frame.paragraphs[0].font.size, shape.text_frame.paragraphs[0].font.color.rgb)