Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ At the moment, the following optional dependencies are available:
* `[audio-transcription]` Installs dependencies for audio transcription of wav and mp3 files
* `[youtube-transcription]` Installs dependencies for fetching YouTube video transcription

For PDFs containing bottom-to-top rotated text, pass the direction expected by
pdfplumber when converting the document:

```python
from markitdown import MarkItDown

result = MarkItDown().convert("rotated.pdf", pdf_char_dir_rotated="btt")
```

### Plugins

MarkItDown also supports 3rd-party plugins. Plugins are disabled by default. To list installed plugins:
Expand Down
37 changes: 31 additions & 6 deletions packages/markitdown/src/markitdown/converters/_pdf_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ def fmt_row(row: list[str]) -> str:
return "\n".join(md)


def _extract_form_content_from_words(page: Any) -> str | None:
def _extract_form_content_from_words(
page: Any, *, char_dir_rotated: str | None = None
) -> str | None:
"""
Extract form-style content from a PDF page by analyzing word positions.
This handles borderless forms/tables where words are aligned in columns.
Expand All @@ -129,7 +131,14 @@ def _extract_form_content_from_words(page: Any) -> str | None:
Returns None if the page doesn't appear to be a form-style document,
indicating that pdfminer should be used instead for better text spacing.
"""
words = page.extract_words(keep_blank_chars=True, x_tolerance=3, y_tolerance=3)
extract_words_kwargs: dict[str, Any] = {
"keep_blank_chars": True,
"x_tolerance": 3,
"y_tolerance": 3,
}
if char_dir_rotated is not None:
extract_words_kwargs["char_dir_rotated"] = char_dir_rotated
words = page.extract_words(**extract_words_kwargs)
if not words:
return None

Expand Down Expand Up @@ -395,15 +404,24 @@ def extract_cells(info: dict) -> list[str]:
return "\n".join(result_lines)


def _extract_tables_from_words(page: Any) -> list[list[list[str]]]:
def _extract_tables_from_words(
page: Any, *, char_dir_rotated: str | None = None
) -> list[list[list[str]]]:
"""
Extract tables from a PDF page by analyzing word positions.
This handles borderless tables where words are aligned in columns.

This function is designed for structured tabular data (like invoices),
not for multi-column text layouts in scientific documents.
"""
words = page.extract_words(keep_blank_chars=True, x_tolerance=3, y_tolerance=3)
extract_words_kwargs: dict[str, Any] = {
"keep_blank_chars": True,
"x_tolerance": 3,
"y_tolerance": 3,
}
if char_dir_rotated is not None:
extract_words_kwargs["char_dir_rotated"] = char_dir_rotated
words = page.extract_words(**extract_words_kwargs)
if not words:
return []

Expand Down Expand Up @@ -536,6 +554,11 @@ def convert(

assert isinstance(file_stream, io.IOBase)

char_dir_rotated = kwargs.get("pdf_char_dir_rotated")
extract_text_kwargs: dict[str, Any] = {}
if char_dir_rotated is not None:
extract_text_kwargs["char_dir_rotated"] = char_dir_rotated

# Read file stream into BytesIO for compatibility with pdfplumber
pdf_bytes = io.BytesIO(file_stream.read())

Expand All @@ -551,15 +574,17 @@ def convert(

with pdfplumber.open(pdf_bytes) as pdf:
for page_idx, page in enumerate(pdf.pages):
page_content = _extract_form_content_from_words(page)
page_content = _extract_form_content_from_words(
page, char_dir_rotated=char_dir_rotated
)

if page_content is not None:
form_page_count += 1
if page_content.strip():
markdown_chunks.append(page_content)
else:
plain_page_indices.append(page_idx)
text = page.extract_text()
text = page.extract_text(**extract_text_kwargs)
if text and text.strip():
markdown_chunks.append(text.strip())

Expand Down
Binary file not shown.
12 changes: 12 additions & 0 deletions packages/markitdown/tests/test_pdf_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1196,3 +1196,15 @@ def test_borderless_table_data_integrity(self, markitdown):
table_text = str(second_table)
assert "Electronics" in table_text, "Second table should contain Electronics"
assert "Hardware" in table_text, "Second table should contain Hardware"

def test_rotated_text_direction_can_be_configured(self, markitdown):
"""Test that bottom-to-top rotated text can use pdfplumber's direction option."""
pdf_path = os.path.join(TEST_FILES_DIR, "rotated_table.pdf")

default_result = markitdown.convert(pdf_path)
assert "noitalupoP" in default_result.text_content

result = markitdown.convert(pdf_path, pdf_char_dir_rotated="btt")
assert "Projected" in result.text_content
assert "Population" in result.text_content
assert "noitalupoP" not in result.text_content