diff --git a/docling/backend/mspowerpoint_backend.py b/docling/backend/mspowerpoint_backend.py index e641f138d6..404acfe0fe 100644 --- a/docling/backend/mspowerpoint_backend.py +++ b/docling/backend/mspowerpoint_backend.py @@ -186,6 +186,15 @@ def _iter_paragraph_elements(element: etree._Element) -> Iterator[etree._Element yield child +def _text_frame_text(text: str) -> str: + """Return the ``.text`` of a python-pptx text frame with "\\n" line breaks. + + python-pptx gives a line break inside a paragraph (``a:br``) as a vertical + tab ("\\v"). + """ + return text.replace("\v", "\n").strip() + + def _paragraph_fragments(element: etree._Element) -> list[tuple[DocItemLabel, str]]: """Coalesce adjacent text runs while keeping equations as separate fragments.""" fragments: list[tuple[DocItemLabel, str]] = [] @@ -1327,8 +1336,9 @@ def _handle_tables(self, shape, parent_slide, slide_ind, doc, slide_size): else: col_span = int(col_span) + cell_text = _text_frame_text(cell.text) icell = TableCell( - text=cell.text.strip(), + text=cell_text, row_span=row_span, col_span=col_span, start_row_offset_idx=row_idx, @@ -1338,7 +1348,7 @@ def _handle_tables(self, shape, parent_slide, slide_ind, doc, slide_size): column_header=row_idx == 0, row_header=False, ) - if len(cell.text.strip()) > 0: + if len(cell_text) > 0: tcells.append(icell) # Initialize Docling TableData data = TableData(num_rows=num_rows, num_cols=num_cols, table_cells=[]) @@ -1999,7 +2009,7 @@ def handle_groups(shape, parent_slide, slide_ind, doc, slide_size): if slide.has_notes_slide: notes_slide = slide.notes_slide if notes_slide.notes_text_frame is not None: - notes_text = notes_slide.notes_text_frame.text.strip() + notes_text = _text_frame_text(notes_slide.notes_text_frame.text) if notes_text: bbox = BoundingBox(l=0, t=0, r=0, b=0) prov = ProvenanceItem( diff --git a/tests/test_backend_pptx.py b/tests/test_backend_pptx.py index e764035487..639de0f676 100644 --- a/tests/test_backend_pptx.py +++ b/tests/test_backend_pptx.py @@ -319,6 +319,31 @@ def test_pptx_left_flush_shape_keeps_own_bbox(tmp_path: Path): assert bbox.r != prs.slide_width +def test_pptx_line_breaks_in_table_cells_and_notes(tmp_path: Path): + """A line break (a:br) in a table cell or in the notes becomes "\\n". + + python-pptx returns a line break inside a paragraph as a vertical tab + ("\\v") in ``.text``, so this control character was in the text of table + cells and notes. + """ + from pptx import Presentation + from pptx.util import Inches + + prs = Presentation() + slide = prs.slides.add_slide(prs.slide_layouts[5]) + shape = slide.shapes.add_table(1, 1, Inches(1), Inches(2), Inches(4), Inches(1)) + shape.table.cell(0, 0).text = "line1\vline2" + slide.notes_slide.notes_text_frame.text = "note a\vnote b" + pptx_path = tmp_path / "line_breaks.pptx" + prs.save(pptx_path) + + doc = get_converter().convert(pptx_path).document + + assert doc.tables[0].data.table_cells[0].text == "line1\nline2" + notes = [t.text for t in doc.texts if t.content_layer == ContentLayer.NOTES] + assert "note a\nnote b" in notes + + def test_pptx_page_range(): converter = get_converter() pptx_path = Path("./tests/data/pptx/sources/powerpoint_sample.pptx")