diff --git a/packages/markitdown-ocr/src/markitdown_ocr/_pdf_converter_with_ocr.py b/packages/markitdown-ocr/src/markitdown_ocr/_pdf_converter_with_ocr.py index c1dc0f613..e79582e67 100644 --- a/packages/markitdown-ocr/src/markitdown_ocr/_pdf_converter_with_ocr.py +++ b/packages/markitdown-ocr/src/markitdown_ocr/_pdf_converter_with_ocr.py @@ -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 "" @@ -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: diff --git a/packages/markitdown-ocr/tests/ocr_test_data/pdf_text_then_blank.pdf b/packages/markitdown-ocr/tests/ocr_test_data/pdf_text_then_blank.pdf new file mode 100644 index 000000000..c64d896b6 Binary files /dev/null and b/packages/markitdown-ocr/tests/ocr_test_data/pdf_text_then_blank.pdf differ diff --git a/packages/markitdown-ocr/tests/test_pdf_converter.py b/packages/markitdown-ocr/tests/test_pdf_converter.py index 5d4adcc5e..5849c0323 100644 --- a/packages/markitdown-ocr/tests/test_pdf_converter.py +++ b/packages/markitdown-ocr/tests/test_pdf_converter.py @@ -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) # ---------------------------------------------------------------------------