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
66 changes: 66 additions & 0 deletions examples/overflow_detection_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Example: Text Overflow Detection

This example demonstrates how to detect when text will overflow a text frame
before applying changes to the presentation.
"""

from pptx import Presentation
from pptx.util import Inches, Pt

# Create a presentation with a text box
prs = Presentation()
blank_slide_layout = prs.slide_layouts[6]
slide = prs.slides.add_slide(blank_slide_layout)

# Add a text box
textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(3))
text_frame = textbox.text_frame
text_frame.word_wrap = True

# Add some content
text_frame.text = "Project Overview"
for i in range(1, 11):
p = text_frame.add_paragraph()
p.text = f"Key point #{i}: Important information about the project"

# Example 1: Simple overflow check
if text_frame.will_overflow(font_family="Arial", font_size=18):
print("Warning: Content will be cut off at 18pt!")
else:
print("All content fits at 18pt")

# Example 2: Get detailed overflow information
info = text_frame.overflow_info(font_family="Arial", font_size=18)

if info.will_overflow:
print(f"\nOverflow detected:")
print(f" Required height: {info.required_height.inches:.2f} inches")
print(f" Available height: {info.available_height.inches:.2f} inches")
print(f" Overflow: {info.overflow_percentage:.1f}%")
print(f" Recommendation: Use {info.fits_at_font_size}pt font to fit")
else:
print("\nAll content fits!")

# Example 3: Find the best font size
for size in [20, 18, 16, 14, 12]:
if not text_frame.will_overflow(font_family="Arial", font_size=size):
print(f"\nLargest fitting font size: {size}pt")
break

# Example 4: Validate before applying font size
desired_size = 18
if text_frame.will_overflow(font_family="Arial", font_size=desired_size):
print(f"\nWarning: Cannot use {desired_size}pt - would overflow!")
info = text_frame.overflow_info(font_family="Arial", font_size=desired_size)
print(f"Using recommended size: {info.fits_at_font_size}pt instead")
# Apply the recommended size
for paragraph in text_frame.paragraphs:
for run in paragraph.runs:
run.font.size = Pt(info.fits_at_font_size)
else:
# Apply the desired size
for paragraph in text_frame.paragraphs:
for run in paragraph.runs:
run.font.size = Pt(desired_size)

prs.save("overflow_example.pptx")
153 changes: 153 additions & 0 deletions features/steps/text_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,57 @@ def given_a_text_frame_with_more_text_than_will_fit(context):
context.text_frame = shape.text_frame


@given("a text frame with text that will overflow at 18pt")
def given_a_text_frame_with_text_that_will_overflow_at_18pt(context):
prs = Presentation(test_pptx("txt-fit-text"))
shape = prs.slides[0].shapes[0]
context.text_frame = shape.text_frame


@given("a text frame with text that will fit at 10pt")
def given_a_text_frame_with_text_that_will_fit_at_10pt(context):
prs = Presentation(test_pptx("txt-fit-text"))
shape = prs.slides[0].shapes[0]
context.text_frame = shape.text_frame


@given("an empty text frame")
def given_an_empty_text_frame(context):
prs = Presentation(test_pptx("txt-text-frame"))
shape = prs.slides[0].shapes[0]
context.text_frame = shape.text_frame
context.text_frame.clear()


@given("a text frame with uniform 14pt text")
def given_a_text_frame_with_uniform_14pt_text(context):
prs = Presentation()
blank_slide_layout = prs.slide_layouts[6]
slide = prs.slides.add_slide(blank_slide_layout)
textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(3))
context.text_frame = textbox.text_frame
context.text_frame.text = "Test text"
for paragraph in context.text_frame.paragraphs:
for run in paragraph.runs:
run.font.size = Pt(14)


@given("a text frame with mixed font sizes")
def given_a_text_frame_with_mixed_font_sizes(context):
prs = Presentation()
blank_slide_layout = prs.slide_layouts[6]
slide = prs.slides.add_slide(blank_slide_layout)
textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(3))
context.text_frame = textbox.text_frame
p = context.text_frame.paragraphs[0]
r1 = p.add_run()
r1.text = "First"
r1.font.size = Pt(14)
r2 = p.add_run()
r2.text = "Second"
r2.font.size = Pt(18)


# when ====================================================


Expand Down Expand Up @@ -84,6 +135,50 @@ def when_I_call_TextFrame_fit_text(context):
# context.text_frame.fit_text(font_family='Arial', bold=True, italic=True)


@when("I call text_frame.will_overflow(font_size={font_size})")
def when_I_call_text_frame_will_overflow_with_font_size(context, font_size):
from helpers import test_file

font_file = test_file("calibriz.ttf")
context.result = context.text_frame.will_overflow(
bold=True, italic=True, font_size=int(font_size), font_file=font_file
)


@when("I call text_frame.will_overflow() without specifying font_size")
def when_I_call_text_frame_will_overflow_without_font_size(context):
from helpers import test_file

font_file = test_file("calibriz.ttf")
try:
context.result = context.text_frame.will_overflow(
bold=True, italic=True, font_file=font_file
)
context.exception = None
except Exception as e:
context.exception = e


@when("I call text_frame.will_overflow()")
def when_I_call_text_frame_will_overflow(context):
from helpers import test_file

font_file = test_file("calibriz.ttf")
context.result = context.text_frame.will_overflow(
bold=True, italic=True, font_file=font_file
)


@when("I call text_frame.overflow_info(font_size={font_size})")
def when_I_call_text_frame_overflow_info_with_font_size(context, font_size):
from helpers import test_file

font_file = test_file("calibriz.ttf")
context.info = context.text_frame.overflow_info(
bold=True, italic=True, font_size=int(font_size), font_file=font_file
)


# then ====================================================


Expand Down Expand Up @@ -127,3 +222,61 @@ def then_the_size_of_the_text_is_10pt(context):
for paragraph in text_frame.paragraphs:
for run in paragraph.runs:
assert run.font.size in (Pt(10.0), Pt(11.0)), "got %s" % run.font.size.pt


@then("it returns True")
def then_it_returns_true(context):
assert context.result is True, "Expected True, got %s" % context.result


@then("it returns False")
def then_it_returns_false(context):
assert context.result is False, "Expected False, got %s" % context.result


@then("info.will_overflow is True")
def then_info_will_overflow_is_true(context):
assert context.info.will_overflow is True


@then("info.will_overflow is False")
def then_info_will_overflow_is_false(context):
assert context.info.will_overflow is False


@then("info.required_height is greater than info.available_height")
def then_info_required_height_is_greater_than_available_height(context):
assert context.info.required_height > context.info.available_height


@then("info.overflow_percentage is greater than 0")
def then_info_overflow_percentage_is_greater_than_0(context):
assert context.info.overflow_percentage > 0


@then("info.fits_at_font_size is less than 18")
def then_info_fits_at_font_size_is_less_than_18(context):
assert context.info.fits_at_font_size < 18


@then("info.overflow_height is 0")
def then_info_overflow_height_is_0(context):
assert context.info.overflow_height == 0


@then("info.fits_at_font_size is None")
def then_info_fits_at_font_size_is_none(context):
assert context.info.fits_at_font_size is None


@then("it uses 14pt as the font size")
def then_it_uses_14pt_as_font_size(context):
# This is verified by not raising an exception
assert context.exception is None


@then("it raises ValueError")
def then_it_raises_value_error(context):
assert context.exception is not None
assert isinstance(context.exception, ValueError)
assert "multiple font sizes" in str(context.exception)
44 changes: 44 additions & 0 deletions features/txt-overflow-detection.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Feature: Detect text overflow
In order to prevent content from being cut off
As a developer using python-pptx
I need a way to detect when text will overflow its shape

Scenario: Detect when text will overflow
Given a text frame with text that will overflow at 18pt
When I call text_frame.will_overflow(font_size=18)
Then it returns True

Scenario: Detect when text will fit
Given a text frame with text that will fit at 10pt
When I call text_frame.will_overflow(font_size=10)
Then it returns False

Scenario: Get detailed overflow information
Given a text frame with text that will overflow at 18pt
When I call text_frame.overflow_info(font_size=18)
Then info.will_overflow is True
And info.required_height is greater than info.available_height
And info.overflow_percentage is greater than 0
And info.fits_at_font_size is less than 18

Scenario: Get overflow info for fitting text
Given a text frame with text that will fit at 10pt
When I call text_frame.overflow_info(font_size=10)
Then info.will_overflow is False
And info.overflow_height is 0
And info.fits_at_font_size is None

Scenario: Handle empty text frame
Given an empty text frame
When I call text_frame.will_overflow()
Then it returns False

Scenario: Use effective font size
Given a text frame with uniform 14pt text
When I call text_frame.will_overflow() without specifying font_size
Then it uses 14pt as the font size

Scenario: Reject mixed font sizes
Given a text frame with mixed font sizes
When I call text_frame.will_overflow() without specifying font_size
Then it raises ValueError
86 changes: 86 additions & 0 deletions src/pptx/text/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,92 @@ def best_fit_font_size(
text_fitter = cls(line_source, extents, font_file)
return text_fitter._best_fit_font_size(max_size)

@classmethod
def will_fit(
cls, text: str, extents: tuple[Length, Length], font_size: int, font_file: str
) -> bool:
"""Return True if text will fit within extents at the given font size.

Args:
text: The text to check
extents: (width, height) tuple in EMUs
font_size: Font size in points
font_file: Path to TrueType font file

Returns:
Boolean indicating if text fits completely within extents
"""
line_source = _LineSource(text)
text_fitter = cls(line_source, extents, font_file)
predicate = text_fitter._fits_inside_predicate
return predicate(font_size)

@classmethod
def measure_text_height(
cls, text: str, extents: tuple[Length, Length], font_size: int, font_file: str
) -> int:
"""Calculate the height required to render text at the given font size.

Args:
text: The text to measure
extents: (width, height) tuple in EMUs
font_size: Font size in points
font_file: Path to TrueType font file

Returns:
Required height in EMUs to fit all text
"""
line_source = _LineSource(text)
text_fitter = cls(line_source, extents, font_file)
text_lines = text_fitter._wrap_lines(line_source, font_size)
line_height = _rendered_size("Ty", font_size, font_file)[1]
return line_height * len(text_lines)

@classmethod
def calculate_overflow_metrics(
cls, text: str, extents: tuple[Length, Length], font_size: int, font_file: str
) -> dict:
"""Calculate detailed overflow metrics for text.

Args:
text: The text to analyze
extents: (width, height) tuple in EMUs
font_size: Font size in points
font_file: Path to TrueType font file

Returns:
Dictionary with keys:
- required_height: Height needed in EMUs
- available_height: Available height in EMUs
- overflow_height: How much text exceeds bounds (0 if fits)
- overflow_percentage: Percentage of overflow
- estimated_lines: Number of wrapped lines
"""
line_source = _LineSource(text)
text_fitter = cls(line_source, extents, font_file)

# Calculate required height
text_lines = text_fitter._wrap_lines(line_source, font_size)
line_height = _rendered_size("Ty", font_size, font_file)[1]
required_height = line_height * len(text_lines)

# Get available height from extents
available_height = extents[1]

# Calculate overflow
overflow_height = max(0, required_height - available_height)
overflow_percentage = (
(overflow_height / available_height * 100) if available_height > 0 else 0
)

return {
"required_height": required_height,
"available_height": available_height,
"overflow_height": overflow_height,
"overflow_percentage": overflow_percentage,
"estimated_lines": len(text_lines),
}

def _best_fit_font_size(self, max_size):
"""
Return the largest whole-number point size less than or equal to
Expand Down
Loading