scanny / python-pptx

Create Open XML PowerPoint documents in Python
MIT License
2.39k stars 518 forks source link

text_frame.fit_text crashes if no wrapped representation fits in the width of the shape #936

Open rianwouters opened 8 months ago

rianwouters commented 8 months ago

I have a shape with with w and height h. The size of the first word in the text when rendered with the maximum allowed text size is bigger than w. In that case, text_fit crashes with the stack trace below, allthough there is a smaller text size that fits.

Exception: cannot unpack non-iterable NoneType object
Traceback (most recent call last):
  File "site-packages\pptx\text\text.py", line 239, 
in _best_fit_font_size
    return TextFitter.best_fit_font_size(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages\pptx\text\layout.py", line 27, in best_fit_font_size
    return text_fitter._best_fit_font_size(max_size)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages\pptx\text\layout.py", line 36, in _best_fit_font_size
    return sizes.find_max(predicate)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages\pptx\text\layout.py", line 130, in find_max
    if predicate(self.value):
       ^^^^^^^^^^^^^^^^^^^^^
  File "site-packages\pptx\text\layout.py", line 79, in predicate
    text_lines = self._wrap_lines(self._line_source, point_size)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages\pptx\text\layout.py", line 107, in _wrap_lines
    text, remainder = self._break_line(line_source, point_size)
MartinPacker commented 8 months ago

As a user (not a python-pptx developer) it strikes me to fix this would be difficult:

It would require python-pptx to ingest font metrics for all the fonts and compute the dimensions of every string in every run that gets added. Even if the metrics were available it's got to slow the code down tremendously.

scanny commented 8 months ago

@rianwouters your stack trace is incomplete. In particular the exception is missing. Also, make sure to use triple-backtick "fences" when adding code to a post.

rianwouters commented 8 months ago

self._break_line(line_source, point_size) returns None en therefore _wrap_lines fails to unpack it.

I believe what should happen is the following:

def predicate(point_size) in layout.py@73 (defined in _fits_inside_predicate) should return false when the text cannot be wrapped to fit because the smallest biggest does not fit within the available width.

Simple fix would be to have a try..except around _wrap_lines to catch this exception and return false.

Alternatively, _wrap_lines needs a way of expressin in the return value that the text cannot be wrapped to fit as requested, for example by returning None in such case. the predicate then needs

if !text_lines:
 return False

_wrap_lines would need something like:

broken_line = self._break_line(line_source, point_size)
if !broken_line:
   return None
text, remainder = broken_line

@MartinPacker This is almost what this code is doing ;-)