From 83f75895b3eb6bf8616b310fe162091eb93c6d9c Mon Sep 17 00:00:00 2001 From: Brumbelow Date: Wed, 22 Apr 2026 13:38:53 -0400 Subject: [PATCH] fix: OCR scanned pages when mixed with text pages --- .../markitdown_ocr/_pdf_converter_with_ocr.py | 28 ++++++++++++++++++ .../ocr_test_data/pdf_text_then_blank.pdf | Bin 0 -> 1762 bytes .../tests/test_pdf_converter.py | 16 ++++++++++ 3 files changed, 44 insertions(+) create mode 100644 packages/markitdown-ocr/tests/ocr_test_data/pdf_text_then_blank.pdf 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 0000000000000000000000000000000000000000..c64d896b6ec29b4cd78fa935fed49408ccdf234a GIT binary patch literal 1762 zcmdT_TX*6%5PtWsm_1oeNH<{1##}a(Znz~hBwPZUwm`bFEx=ijd}Mi>^r3&Ced~LF zYe!rXm*$*3r*HLxr5TMz^Nqe4W%|u_j^rI9^XDJG{{=%938}i&gA2x)q>2TA77z$O zSu?I6DxlUu&7U;mdWUDVsg+2wd z#eETkd;#WN$j^Bkz9??!nt^s|-X@-UiclEgA7wq@3i?VN<)n^k;1Y%!Ls`cI<~0jK zS{IKnLD_;Lv7uD4VFqTON{y+swHvZ6t{66Wbg&vAdgx2x-!g>`Oq~pK#2yhyXH6HZ zz8f36u>&&<3(TQVm^|EWjwIz9ms)SY9zY*ArvMbtbf2qd0F~|W{|f+a3V_CTEdsoz zam;Q6$B94T#Nuvm>%WTtCoMwV%;|mjT2iBE& z6=L8M&@>-{=E_j>cbmseYExO%wBI{uhbx5;khtf!jZUZRHW`+juAdd=a3`RrF5?Te z1mf7*QLHf0YvY1OA6gdu)Ju-LllM*D^D^})xNaUV6$_ zUTJpLwY^*+Z_dW!Z!*p#f7TndnycDfH>3Rd(e$TkuPc}HEHCBG!``QIvM$wIchSd- zFyD#T>G-Jn{SR;AceBs!V6wcd{#L_QvWsTdRCG=JM|5uGpWp^dtMzUB)EZAOozXP9 z`EhhVxcdB^@@t*`>Dk@UnB3b=xa_yCfA06+_TD)6rww=TN=%QE%?%5PWmwN26kXSG zVGhr_B6bt~0e{h9A3D^L3PL=vb6&A?%UE&;c10H%a-gdaQA*Ny8PB zo6g>8f-$KwuUsyZU`~S)aV^&?lq((y9M2_$76MYKecfVaWKs{z;`Ik6mxCb0UUBf$ q7-omh1?UbjG_bo01-j$l{5JfR>C|s{OQqUe25|}%Ba> 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) # ---------------------------------------------------------------------------