Skip to content
Open
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
12 changes: 10 additions & 2 deletions src/pptx/text/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def predicate(point_size):
when rendered at `point_size` using the font defined in `font_file`.
"""
text_lines = self._wrap_lines(self._line_source, point_size)
if text_lines is None:
return False
cy = _rendered_size("Ty", point_size, self._font_file)[1]
return (cy * len(text_lines)) <= self._height

Expand Down Expand Up @@ -109,10 +111,16 @@ def _wrap_lines(self, line_source, point_size):
*line_source* wrapped within this fitter when rendered at
*point_size*.
"""
text, remainder = self._break_line(line_source, point_size)
result = self._break_line(line_source, point_size)
if result is None:
return None
text, remainder = result
lines = [text]
if remainder:
lines.extend(self._wrap_lines(remainder, point_size))
remaining_lines = self._wrap_lines(remainder, point_size)
if remaining_lines is None:
return None
lines.extend(remaining_lines)
return lines


Expand Down
40 changes: 40 additions & 0 deletions src/pptx/text/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,44 @@ def fit_text(
font_size = self._best_fit_font_size(font_family, max_size, bold, italic, font_file)
self._apply_fit(font_family, font_size, bold, italic)

def shrink_text_to_fit(
self,
font_family: str = "Calibri",
max_size: int = 18,
bold: bool = False,
italic: bool = False,
font_file: str | None = None,
min_size: int = 6,
):
"""Shrink text to fit within bounds and enable TEXT_TO_FIT_SHAPE autofit.

Similar to :meth:`fit_text`, but sets :attr:`TextFrame.auto_size` to
:attr:`MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE` so PowerPoint will continue to
auto-shrink text if it's modified later.

This method calculates the best-fit font size and applies it immediately,
solving the issue where PowerPoint doesn't recalculate text size on file open.

:attr:`TextFrame.auto_size` is set to :attr:`MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`.
The font size will not be set larger than `max_size` points or smaller than
`min_size` points.
"""
if self.text == "":
return

try:
font_size = self._best_fit_font_size(font_family, max_size, bold, italic, font_file)
if font_size is None:
font_size = min_size
else:
font_size = max(font_size - 2, min_size)
except (TypeError, RecursionError):
font_size = min_size

self.word_wrap = True
self._set_font(font_family, font_size, bold, italic)
self.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE

@property
def margin_bottom(self) -> Length:
"""|Length| value representing the inset of text from the bottom text frame border.
Expand Down Expand Up @@ -261,6 +299,8 @@ def _set_font(self, family: str, size: int, bold: bool, italic: bool):

def iter_rPrs(txBody: CT_TextBody) -> Iterator[CT_TextCharacterProperties]:
for p in txBody.p_lst:
pPr = p.get_or_add_pPr()
yield pPr.get_or_add_defRPr()
for elm in p.content_children:
yield elm.get_or_add_rPr()
# generate a:endParaRPr for each <a:p> element
Expand Down
28 changes: 28 additions & 0 deletions tests/text/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,34 @@ def it_can_resize_its_text_to_best_fit(self, request, text_prop_):
)
_apply_fit_.assert_called_once_with(text_frame, family, font_size, bold, italic)

def it_fits_text_and_shrinks_with_autosize_enabled(self, request):
"""Test shrink_text_to_fit calculates font size and sets TEXT_TO_FIT_SHAPE."""
family, max_size, bold, italic, font_file, font_size = (
"family",
42,
"bold",
"italic",
"font_file",
21,
)
expected_font_size = font_size - 2
text_prop_ = property_mock(request, TextFrame, "text")
text_prop_.return_value = "some text"
_best_fit_font_size_ = method_mock(
request, TextFrame, "_best_fit_font_size", return_value=font_size
)
_set_font_ = method_mock(request, TextFrame, "_set_font")
text_frame = TextFrame(element("p:txBody/a:bodyPr"), None)

text_frame.shrink_text_to_fit(family, max_size, bold, italic, font_file)

_best_fit_font_size_.assert_called_once_with(
text_frame, family, max_size, bold, italic, font_file
)
_set_font_.assert_called_once_with(text_frame, family, expected_font_size, bold, italic)
assert text_frame.word_wrap is True
assert text_frame.auto_size is MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE

def it_calculates_its_best_fit_font_size_to_help_fit_text(self, size_font_fixture):
text_frame, family, max_size, bold, italic = size_font_fixture[:5]
FontFiles_, TextFitter_, text, extents = size_font_fixture[5:9]
Expand Down