From a0306392191bdf22fc003360acd9f92d5d4dd09e Mon Sep 17 00:00:00 2001 From: LindseyZ1205 Date: Wed, 14 Oct 2026 23:17:26 -0400 Subject: [PATCH] fix(pptx): turn line breaks in table cells and notes into newlines python-pptx gives a line break inside a paragraph (a:br) as a vertical tab ("\v") in the .text of a text frame. The backend used .text for table cells and notes, so this control character ended up in the cell text, in the notes text and in the Markdown export. Replace it with "\n", the in-paragraph line break of a DoclingDocument (#1787). Signed-off-by: LindseyZ1205 --- docling/backend/mspowerpoint_backend.py | 16 +++++++++++++--- tests/test_backend_pptx.py | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) 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")