Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for not committing the dependency lock file? I would only not do this if there is a problem...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we use requirements.txt and dev_requirements.txt for listing dependencies a Pipfile would duplicate the requirements.


# celery beat schedule file
celerybeat-schedule
Expand Down Expand Up @@ -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
Binary file added playground/pptx_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions playground/testing_1.py
Original file line number Diff line number Diff line change
@@ -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)
37 changes: 35 additions & 2 deletions pptx_blueprint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an Exception. Don't let errors pass silently!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a good idea to handle the problem on a higher level. I will replace return by

raise ValueError(f"The label '{label}' can't be found in the template.")

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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
img_file = open(filename, "rb")
with open(filename, "rb") as img_file:

old_shape: BaseShape
Copy link
Collaborator

@lysnikolaou lysnikolaou Oct 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would maybe type shapes_to_replace, instead of old_shape.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already use shapes_to_replace for the iterator that is returned by _find_shapes(). In my opinion the singular version shape_to_replace is to close to shapes_to_replace and therefor old_shape makes it easier to distinguish between the iterator and the single shape.

for old_shape in shapes_to_replace:
slide_shapes = old_shape._parent
Copy link

@cmahr cmahr Oct 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of using the internals of the pptx library. But if there is really no better way...

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems there is currently no other solution:
scanny/python-pptx#246

Could be mentionend in a code comment.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for linking the resource.

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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes! We should really not deal with xml ourselves. We should open a RP in pptx to get this feature implemented. For the time being, we should either 1) isolate this in a private method marked deprecated to prevent excessive use, or 2) decorate the pptx presentation object.


def replace_table(self, label: str, data) -> None:
"""Replaces rectangle placeholders on one or many slides.
Expand Down