"""Main Module"""
import re
from os import PathLike
from pathlib import Path
from typing import Any, Dict, Optional, Union, Callable, List
from warnings import warn as warning
from functools import partial
from . import plugins
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from .exceptions import RenderError
from .utils import fix_quotes, para_text_replace, copy_slide, clear_presentation
PLUGINS = [plugins.image, plugins.video, plugins.table]
[docs]class PPTXRenderer:
"""PPTX Renderer class
This class is used to render a PPTX template by replacing python statements
with the result of evaluating the python statements.
Attributes:
template_path (str): Path to the PPTX template.
"""
def __init__(self, template_path: Union[str, PathLike]):
self.template_path = template_path
self.plugins = {}
self.namespace = {}
for plugin in PLUGINS:
self.register_plugin(plugin.__name__, plugin)
[docs] def register_plugin(self, name: str, func: Callable):
"""Register a plugin function.
The plugin function should take 2 or more arguments. The first argument
is the result of evaluating the python statement. The second argument is
a dictionary containing the following keys:
- result: The result of evaluating the python statement
- presentation: The output pptx presentation object
- shape: The pptx shape object where the placeholder is present
- slide: The pptx slide object where the placeholder is present
- slide_no: The slide number where the placeholder is present
The remaining arguments are the arguments passed to the plugin function
Args:
name (str): Name of the plugin.
func (callable): Function to be registered.
Returns:
None
"""
self.plugins[name] = func
[docs] def render(
self,
output_path: Union[str, PathLike],
methods_and_params: Optional[Dict[str, Any]] = None,
skip_failed: bool = False,
loop_groups: Optional[List[Dict[str, Any]]] = None,
) -> None:
"""Render PPTXRenderer template and save to output_path.
Args:
output_path (str): Path to the output PPTX file.
methods_and_params (dict, optional): Dictionary of methods and parameters
to be used in the template. Defaults to None.
skip_failed (bool, optional): Dont raise an error if some of the
statements failed to render. Defaults to False.
loop_groups (list, optional): List of dictionaries containing the
following keys:
- start: Slide number where the loop starts
- end: Slide number where the loop ends
- variable: Variable name to be used in the loop
- iterable: Iterable to loop over
Defaults to None.
Returns:
None
"""
self.template_path = str(self.template_path)
if not Path(self.template_path).exists():
raise (FileNotFoundError(f"{self.template_path} not found"))
template_ppt = Presentation(self.template_path)
if not methods_and_params:
methods_and_params = {}
self.namespace.update(methods_and_params)
#function to recurse through list of shapes
#hand off to function to process text frames and tables
#recurse into handle_shapes function if group of shapes are found
def handle_shapes(shapes):
for shape in list(shapes):
if shape.has_text_frame:
handle_text_frame(shape)
if shape.has_table:
handle_table(shape)
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
handle_shapes(shape.shapes)
def handle_text_frame(shape):
matches = re.finditer(r"{{{(.*?)}}}", shape.text)
if not matches:
return
for match_assignment in matches:
parts = match_assignment.group(1).split(":", 1)
try:
result = eval(fix_quotes(parts[0]), self.namespace)
except Exception as ex:
if skip_failed:
warning(
f"Evaluation of '{parts[0]}' in slide {slide_no+1} failed"
)
return
raise RenderError(
f"Failed to evaluate '{parts[0]}' in slide {slide_no+1}."
) from ex
if len(parts) > 1:
namespace = self.namespace.copy()
context = {
"result": result,
"presentation": template_ppt,
"shape": shape,
"slide": slide,
"slide_no": slide_no,
}
for plugin_name, plugin in self.plugins.items():
func = partial(plugin, context)
namespace[plugin_name] = func
try:
exec(fix_quotes(parts[1]), namespace)
except Exception as ex:
if skip_failed:
warning(
f"Failed to render {parts[0]} in slide {slide_no+1}"
)
return
raise RenderError(
f"Failed to render {parts[0]} in slide {slide_no+1}"
) from ex
else:
for paragraph in shape.text_frame.paragraphs:
para_text_replace(
paragraph, match_assignment.group(0), result
)
def handle_table(shape):
for row in shape.table.rows:
for cell in row.cells:
matches = re.finditer(r"{{{(.*)}}}", cell.text)
if not matches:
continue
for match_assignment in matches:
parts = match_assignment.group(1).split(":", 1)
try:
result = eval(fix_quotes(parts[0]), self.namespace)
except Exception as ex:
if skip_failed:
warning(
f"Evaluation of '{parts[0]}' in slide {slide_no+1} failed"
)
continue
raise RenderError(
f"Failed to evaluate '{parts[0]}'."
) from ex
for paragraph in cell.text_frame.paragraphs:
para_text_replace(
paragraph, match_assignment.group(0), result
)
output_ppt = Presentation(self.template_path)
if loop_groups:
clear_presentation(output_ppt)
extra_namespace = {}
slides_managed = []
for slide_no, slide in enumerate(template_ppt.slides):
if slide_no in slides_managed:
# this slide has already been looped over
continue
slide_used = False
for loop_group in loop_groups:
if slide_no == loop_group["start"]:
slide_used = True
for variable_value in loop_group["iterable"]:
for loop_slide_no in range(
loop_group["start"], loop_group["end"] + 1
):
if loop_slide_no not in slides_managed:
slides_managed.append(loop_slide_no)
# get the slide from the template
current_slide = template_ppt.slides[loop_slide_no]
# add a copy of this slide to output_slides
new_slide = copy_slide(template_ppt, output_ppt, current_slide)
extra_namespace[output_ppt.slides.index(new_slide)] = {
loop_group["variable"]: variable_value
}
if not slide_used:
# this slide is not part of a loop group
new_slide = copy_slide(template_ppt, output_ppt, slide)
else:
extra_namespace = {}
for slide_no, slide in enumerate(output_ppt.slides):
self.namespace.update(extra_namespace.get(slide_no, {}))
if slide.has_notes_slide and slide.notes_slide.notes_text_frame:
python_code = re.search(
r"```python([\s\S]*)```",
fix_quotes(slide.notes_slide.notes_text_frame.text),
re.MULTILINE,
)
if python_code:
exec(python_code.group(1), self.namespace)
#send list of shapes to handle_shapes function
handle_shapes(list(slide.shapes))
output_ppt.save(output_path)