diff --git a/haystack/components/converters/pptx.py b/haystack/components/converters/pptx.py index c3a32f2079..3f651c7133 100644 --- a/haystack/components/converters/pptx.py +++ b/haystack/components/converters/pptx.py @@ -17,12 +17,13 @@ with LazyImport("Run 'pip install python-pptx'") as pptx_import: from pptx import Presentation + from pptx.oxml.text import CT_RegularTextRun, CT_TextField from pptx.shapes.autoshape import Shape from pptx.shapes.base import BaseShape from pptx.shapes.graphfrm import GraphicFrame from pptx.shapes.group import GroupShape from pptx.table import Table - from pptx.text.text import _Paragraph + from pptx.text.text import _Paragraph, _Run logger = logging.getLogger(__name__) @@ -137,7 +138,13 @@ def _process_paragraph(self, paragraph: "_Paragraph") -> str: if self.link_format == "none": return paragraph.text parts = [] - for run in paragraph.runs: + for child in paragraph._p.iterchildren(): + if isinstance(child, CT_TextField): + parts.append(child.text) + continue + if not isinstance(child, CT_RegularTextRun): + continue + run = _Run(child, paragraph) if run.hyperlink and run.hyperlink.address: if self.link_format == "markdown": parts.append(f"[{run.text}]({run.hyperlink.address})") diff --git a/releasenotes/notes/pptx-field-text-30f1b392c307906b.yaml b/releasenotes/notes/pptx-field-text-30f1b392c307906b.yaml new file mode 100644 index 0000000000..c04867fc20 --- /dev/null +++ b/releasenotes/notes/pptx-field-text-30f1b392c307906b.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + ``PPTXToDocument`` now keeps the text of a field, such as a slide number or a date, + when ``link_format`` is ``"markdown"`` or ``"plain"``. These formats read only the + runs of a paragraph, so the text of a field between runs was dropped, for example + the ``3`` in ``Slide 3``. The default ``link_format="none"`` already kept field text. diff --git a/test/components/converters/test_pptx_to_document.py b/test/components/converters/test_pptx_to_document.py index cd787f873f..b317d26a63 100644 --- a/test/components/converters/test_pptx_to_document.py +++ b/test/components/converters/test_pptx_to_document.py @@ -2,11 +2,16 @@ # # SPDX-License-Identifier: Apache-2.0 +import io import logging import os from pathlib import Path import pytest +from pptx import Presentation +from pptx.oxml import parse_xml +from pptx.oxml.ns import nsdecls +from pptx.util import Inches from haystack.components.converters.pptx import PPTXToDocument from haystack.dataclasses import ByteStream @@ -124,6 +129,37 @@ def test_no_link_extraction(self, test_files_path): assert "https://example.com" not in content assert "Example" in content + @pytest.mark.parametrize( + "link_format,expected", + [ + ("none", "Slide 3 of the deck"), + ("markdown", "Slide 3 of [the deck](https://example.com)"), + ("plain", "Slide 3 of the deck (https://example.com)"), + ], + ) + def test_run_keeps_field_text_with_every_link_format(self, link_format, expected): + # A slide-number field (`a:fld`) sits between the runs of the paragraph + presentation = Presentation() + slide = presentation.slides.add_slide(presentation.slide_layouts[6]) + paragraph = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(5), Inches(1)).text_frame.paragraphs[0] + paragraph.add_run().text = "Slide " + paragraph._p.append( + parse_xml( + f'' + "3" + ) + ) + paragraph.add_run().text = " of " + link = paragraph.add_run() + link.text = "the deck" + link.hyperlink.address = "https://example.com" + buffer = io.BytesIO() + presentation.save(buffer) + + documents = PPTXToDocument(link_format=link_format).run(sources=[ByteStream(buffer.getvalue())])["documents"] + + assert documents[0].content == expected + def test_run_reads_nested_groups_and_table_with_links(self, test_files_path): # The slide includes nested text groups, a grouped table, and an empty decorative shape. converter = PPTXToDocument(link_format="markdown")