Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ def convert(
text_content = page.extract_text() or ""
if text_content.strip():
markdown_content.append(text_content.strip())
else:
# Scanned page with no image objects or text layer:
# render the whole page and OCR it, so it doesn't
# silently vanish from the output.
page_ocr = self._ocr_page(page, ocr_service)
if page_ocr:
markdown_content.append(page_ocr)
else:
# No OCR, just extract text
text_content = page.extract_text() or ""
Expand Down Expand Up @@ -337,6 +344,27 @@ def _extract_page_images(self, pdf_bytes: io.BytesIO, page_num: int) -> list[dic

return images

def _ocr_page(self, page: Any, ocr_service: LLMVisionOCRService) -> str:
"""
Render a single pdfplumber page to PNG and run OCR on it.

Returns a formatted OCR block, a 'no text' marker if OCR is empty,
or an error marker if rendering/OCR raises.
"""
try:
page_img = page.to_image(resolution=300)
img_stream = io.BytesIO()
page_img.original.save(img_stream, format="PNG")
img_stream.seek(0)

ocr_result = ocr_service.extract_text(img_stream)
text = (ocr_result.text or "").strip()
if text:
return f"*[Image OCR]\n{text}\n[End OCR]*"
return "*[No text could be extracted from this page]*"
except Exception as e:
return f"*[Error processing page {page.page_number}: {str(e)}]*"

def _ocr_full_pages(
self, pdf_bytes: io.BytesIO, ocr_service: LLMVisionOCRService
) -> str:
Expand Down
Binary file not shown.
16 changes: 16 additions & 0 deletions packages/markitdown-ocr/tests/test_pdf_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,22 @@ def test_pdf_scanned_report(svc: MockOCRService) -> None:
assert _convert("pdf_scanned_report.pdf", svc) == expected


# ---------------------------------------------------------------------------
# Mixed text + scanned pages: a page with neither image objects nor a text
# layer must still be OCR'd inline rather than silently dropped.
# Regression test for microsoft/markitdown#1791.
# ---------------------------------------------------------------------------


def test_pdf_text_then_blank_page_is_ocred(svc: MockOCRService) -> None:
expected = (
"## Page 1\n\n\n"
"First page content, extracted as text.\n\n\n"
f"## Page 2\n\n\n{_OCR_BLOCK}"
)
assert _convert("pdf_text_then_blank.pdf", svc) == expected


# ---------------------------------------------------------------------------
# Scanned PDF fallback path (pdfplumber finds no text → full-page OCR)
# ---------------------------------------------------------------------------
Expand Down