Skip to content
Merged
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
52 changes: 52 additions & 0 deletions packages/markitdown/tests/_test_vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,58 @@ class FileTestVector(object):
],
must_not_include=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQE"],
),
FileTestVector(
filename="pptx_empty_title_run.pptx",
mimetype="application/vnd.openxmlformats-officedocument.presentationml.presentation",
charset=None,
url=None,
Comment on lines +75 to +79
must_include=[
"AutoGen: Enabling Next-Gen LLM Applications",
"Text after empty shape",
"Preserved body text",
"### Notes:\nPreserved speaker notes",
],
must_not_include=["Preserved slide title", "None"],
),
FileTestVector(
filename="pptx_empty_body_run.pptx",
mimetype="application/vnd.openxmlformats-officedocument.presentationml.presentation",
charset=None,
url=None,
must_include=[
"AutoGen: Enabling Next-Gen LLM Applications",
"Text after empty shape",
"# Preserved slide title",
"### Notes:\nPreserved speaker notes",
],
must_not_include=["Preserved body text", "None"],
),
FileTestVector(
filename="pptx_notes_empty_run.pptx",
mimetype="application/vnd.openxmlformats-officedocument.presentationml.presentation",
charset=None,
url=None,
must_include=[
"AutoGen: Enabling Next-Gen LLM Applications",
"Text after empty shape",
"# Preserved slide title",
"Preserved body text",
],
must_not_include=["Preserved speaker notes", "None"],
),
FileTestVector(
filename="pptx_notes_missing_body_frame.pptx",
mimetype="application/vnd.openxmlformats-officedocument.presentationml.presentation",
charset=None,
url=None,
must_include=[
"AutoGen: Enabling Next-Gen LLM Applications",
"Text after empty shape",
"# Preserved slide title",
"Preserved body text",
],
must_not_include=["Preserved speaker notes", "None"],
),
FileTestVector(
filename="test_outlook_msg.msg",
mimetype="application/vnd.ms-outlook",
Expand Down
6 changes: 4 additions & 2 deletions packages/markitdown/tests/test_cli_vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ def test_input_from_stdin_without_hints(shared_tmp_dir, test_vector) -> None:
text=False,
)

stdout = result.stdout.decode(locale.getpreferredencoding())
# Binary capture preserves Windows CRLF; match text-mode capture semantics.
stdout = result.stdout.decode(locale.getpreferredencoding()).replace("\r\n", "\n")
assert (
result.returncode == 0
), f"CLI exited with error: {result.stderr.decode('utf-8')}"
Expand Down Expand Up @@ -145,7 +146,8 @@ def test_convert_url(shared_tmp_dir, test_vector):
text=False,
)

stdout = result.stdout.decode(locale.getpreferredencoding())
# Binary capture preserves Windows CRLF; match text-mode capture semantics.
stdout = result.stdout.decode(locale.getpreferredencoding()).replace("\r\n", "\n")
assert result.returncode == 0, f"CLI exited with error: {result.stderr}"
for test_string in test_vector.must_include:
assert test_string in stdout
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Rebuild missing-text PPTX fixtures from copies of test.pptx.

Requires python-pptx. The source fixture is never overwritten.
"""

from pathlib import Path

from pptx import Presentation
from pptx.util import Inches


FIXTURES = Path(__file__).resolve().parent


def _missing_text_presentation() -> Presentation:
# Modify a copy in memory; keep the checked-in deck unchanged.
presentation = Presentation(FIXTURES / "test.pptx")
slide = presentation.slides.add_slide(presentation.slide_layouts[1])
slide.shapes.title.text = "Preserved slide title"
slide.placeholders[1].text = "Preserved body text"
slide.shapes.add_textbox(
Inches(1), Inches(5), Inches(5), Inches(1)
).text = "Text after empty shape"
slide.notes_slide.notes_text_frame.text = "Preserved speaker notes"
return presentation


def main() -> None:
for empty_shape in ("title", "body"):
presentation = _missing_text_presentation()
slide = presentation.slides[-1]
shape = slide.shapes.title if empty_shape == "title" else slide.placeholders[1]
shape.text_frame.clear()
shape.text_frame.paragraphs[0].add_run().text = ""
presentation.save(FIXTURES / f"pptx_empty_{empty_shape}_run.pptx")

for notes_state in ("empty_run", "missing_body_frame"):
presentation = _missing_text_presentation()
notes_slide = presentation.slides[-1].notes_slide
if notes_state == "empty_run":
frame = notes_slide.notes_text_frame
frame.clear()
frame.paragraphs[0].add_run().text = ""
else:
body = notes_slide.notes_placeholder
body._element.getparent().remove(body._element)
presentation.save(FIXTURES / f"pptx_notes_{notes_state}.pptx")


if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
80 changes: 29 additions & 51 deletions packages/markitdown/tests/test_pptx.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from unittest.mock import MagicMock, Mock

import pptx
import pptx.shapes.autoshape
import pptx.slide
import pytest
from bs4 import BeautifulSoup
from lxml import etree
Expand Down Expand Up @@ -151,63 +149,43 @@ def test_notes_with_text_are_still_emitted() -> None:

# Missing text

# Regression test for #1808: PptxConverter must not crash when ``shape.text``
# or ``notes_text_frame.text`` returns ``None`` (e.g. an ``<a:r>`` run with no
# ``<a:t>`` child, or certain third-party-generated decks).
# Empty XML text nodes should not drop surrounding slide content. python-pptx
# normalizes <a:t/> to ""; it does not return None for shape/text-frame text.

TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "test_files")

PPTX_FIXTURE = os.path.join(TEST_FILES_DIR, "test.pptx")


def _build_pptx_with_notes() -> io.BytesIO:
"""Build a minimal pptx with a notes slide attached, in memory."""
prs = pptx.Presentation()
blank = prs.slide_layouts[6]
slide = prs.slides.add_slide(blank)
slide.notes_slide.notes_text_frame.text = "some notes"
buf = io.BytesIO()
prs.save(buf)
buf.seek(0)
return buf


def test_pptx_converter_handles_none_shape_text(monkeypatch):
"""``shape.text`` returning None must not blow up the title / body paths."""
monkeypatch.setattr(
pptx.shapes.autoshape.Shape,
"text",
property(lambda self: None),
raising=True,
)

result = MarkItDown().convert(PPTX_FIXTURE)
assert result.markdown is not None


def test_pptx_converter_handles_none_notes_text(monkeypatch):
"""``notes_text_frame.text`` returning None must not blow up the notes path."""
real_notes = pptx.slide.NotesSlide.notes_text_frame

class _NoneText:
text = None

def fake_notes(self):
frame = real_notes.fget(self)
if frame is None:
return None
return _NoneText()

monkeypatch.setattr(
pptx.slide.NotesSlide,
"notes_text_frame",
property(fake_notes),
raising=True,
@pytest.mark.parametrize("empty_shape", ["title", "body"])
def test_pptx_fixture_contains_empty_text_run(empty_shape: str) -> None:
path = Path(TEST_FILES_DIR) / f"pptx_empty_{empty_shape}_run.pptx"
# Confirm the fixture contains an empty text node, not just no runs.
reloaded = Presentation(path).slides[-1]
shape = (
reloaded.shapes.title if empty_shape == "title" else reloaded.placeholders[1]
)
assert shape.text_frame.paragraphs[0].runs[0]._r.t.text is None
assert shape.text == ""


@pytest.mark.parametrize("notes_state", ["empty-run", "missing-body-frame"])
def test_pptx_empty_notes_have_no_heading(notes_state: str) -> None:
suffix = notes_state.replace("-", "_")
path = Path(TEST_FILES_DIR) / f"pptx_notes_{suffix}.pptx"
reloaded = Presentation(path).slides[-1]
assert reloaded.has_notes_slide
frame = reloaded.notes_slide.notes_text_frame
if notes_state == "empty-run":
assert frame.paragraphs[0].runs[0]._r.t.text is None
assert frame.text == ""
else:
assert frame is None
markdown = MarkItDown().convert(path).markdown

buf = _build_pptx_with_notes()
result = MarkItDown().convert_stream(buf, file_extension=".pptx")
assert result.markdown is not None
# Earlier slides retain their notes; only the appended slide has empty notes.
last_slide = markdown.rsplit("<!-- Slide number:", 1)[1]
assert "### Notes:" not in last_slide


# SVG pictures
Expand Down
Loading