diff --git a/.gitignore b/.gitignore index da844aa..490e4ab 100644 --- a/.gitignore +++ b/.gitignore @@ -90,7 +90,8 @@ target/ # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +Pipfile +Pipfile.lock # celery beat schedule file celerybeat-schedule @@ -128,4 +129,7 @@ dmypy.json # Ignore all local history of files .history +### PyCharm ### +.idea/* + # End of https://www.gitignore.io/api/python,jupyternotebooks,visualstudiocode diff --git a/playground/pptx_icon.png b/playground/pptx_icon.png new file mode 100644 index 0000000..82f9ba7 Binary files /dev/null and b/playground/pptx_icon.png differ diff --git a/playground/testing_1.py b/playground/testing_1.py new file mode 100644 index 0000000..ed425d0 --- /dev/null +++ b/playground/testing_1.py @@ -0,0 +1,64 @@ +from pathlib import Path +from typing import List + +from pptx import Presentation +from pptx.shapes.autoshape import BaseShape +from pptx.slide import Slide + +example_file_path = Path("./data/example01.pptx").resolve() +prs = Presentation(example_file_path) +print(prs) + + +def get_shape_info(shape: BaseShape) -> dict: + return { + "name": shape.name, + "text": shape.text if shape.has_text_frame else None, + "width": shape.width, + "height": shape.height, + "id": shape.shape_id, + "x": shape.left, + "y": shape.top, + "type": type(shape), + } + + +slide_info = [get_shape_info(shape) for shape in prs.slides[0].shapes] +print(slide_info) + + +def replace_by_image(slide: Slide, name: str, img_path: Path, *, do_not_scale: bool = False) -> List[BaseShape]: + shapes_to_replace = [shape for shape in slide.shapes if hasattr(shape, "text") and shape.text == name] + print(len(shapes_to_replace)) + new_image_shapes = [] + for old_shape in shapes_to_replace: + shape_info = get_shape_info(old_shape) + print(shape_info) + img_file = open(img_path, "rb") + slide_shapes = old_shape._parent + img_shape = slide_shapes.add_picture( + img_file, + old_shape.left, + old_shape.top, + ) + old_aspect_ratio = old_shape.width / old_shape.height + new_aspect_ratio = img_shape.width / img_shape.height + if img_shape.height <= old_shape.height and img_shape.width <= old_shape.width and not do_not_scale: + if old_aspect_ratio >= new_aspect_ratio: + img_shape.width = old_shape.width + img_shape.height = int(img_shape.width / new_aspect_ratio) + else: + img_shape.height = old_shape.height + img_shape.width = int(img_shape.height * new_aspect_ratio) + img_shape.top += int((old_shape.height - img_shape.height) / 2) + img_shape.left += int((old_shape.width - img_shape.width) / 2) + new_image_shapes.append(img_shape) + slide_shapes.element.remove(old_shape.element) + return new_image_shapes + + +added_img_shapes = replace_by_image(prs.slides[0], "#logo", Path("./playground/pptx_icon.png")) +assert len(added_img_shapes) == 1 +added_img_shapes = replace_by_image(prs.slides[0], "#logo", Path("./playground/pptx_icon.png")) +assert len(added_img_shapes) == 0 +prs.save(Path("./playground") / example_file_path.name) diff --git a/pptx_blueprint/__init__.py b/pptx_blueprint/__init__.py index 56f0c30..6276edc 100644 --- a/pptx_blueprint/__init__.py +++ b/pptx_blueprint/__init__.py @@ -30,14 +30,47 @@ def replace_text(self, label: str, text: str, *, scope=None) -> None: """ pass - def replace_picture(self, label: str, filename: _Pathlike) -> None: + def replace_picture(self, label: str, filename: _Pathlike, *, do_not_scale_up: bool = False) -> None: """Replaces rectangle placeholders on one or many slides. Args: label (str): label of the placeholder (without curly braces) filename (path-like): path to an image file + do_not_scale_up (bool): deactivates that the image is enlarged (default: False) """ - pass + shapes_to_replace = self._find_shapes(label) + if not shapes_to_replace: + return + if isinstance(filename, str): + filename = pathlib.Path(filename) + if not filename.is_file(): + raise FileNotFoundError(f"The file does not exist: {filename}") + img_file = open(filename, "rb") + old_shape: BaseShape + for old_shape in shapes_to_replace: + slide_shapes = old_shape._parent + img_shape = slide_shapes.add_picture( + image_file=img_file, + left=old_shape.left, + top=old_shape.top, + ) + # Scaling the image if `do_not_scale == False`: + if img_shape.height <= old_shape.height and img_shape.width <= old_shape.width and not do_not_scale_up: + old_aspect_ratio = old_shape.width / old_shape.height + new_aspect_ratio = img_shape.width / img_shape.height + if old_aspect_ratio >= new_aspect_ratio: + img_shape.width = old_shape.width + img_shape.height = int(img_shape.width / new_aspect_ratio) + else: + img_shape.height = old_shape.height + img_shape.width = int(img_shape.height * new_aspect_ratio) + # Centering the image at the extent of the placeholder: + img_shape.top += int((old_shape.height - img_shape.height) / 2) + img_shape.left += int((old_shape.width - img_shape.width) / 2) + del slide_shapes[slide_shapes.index(old_shape)] + # Removing shapes is performed at the lxml level. + # The `element` attribute contains an instance of `lxml.etree._Element`. + slide_shapes.element.remove(old_shape.element) def replace_table(self, label: str, data) -> None: """Replaces rectangle placeholders on one or many slides.