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 @@ -193,45 +193,16 @@ def convert(
images_on_page = self._extract_page_images(pdf_bytes, page_num)

if images_on_page:
# Extract text lines with Y positions
chars = page.chars
if chars:
# Group chars into lines based on Y position
lines_with_y = []
current_line = []
current_y = None

for char in sorted(
chars, key=lambda c: (c["top"], c["x0"])
):
y = char["top"]
if current_y is None:
current_y = y
elif abs(y - current_y) > 2: # New line threshold
if current_line:
text = "".join(
[c["text"] for c in current_line]
)
lines_with_y.append(
{"y": current_y, "text": text.strip()}
)
current_line = []
current_y = y
current_line.append(char)

# Add last line
if current_line:
text = "".join([c["text"] for c in current_line])
lines_with_y.append(
{"y": current_y, "text": text.strip()}
)
else:
# Fallback: use simple text extraction
text_content = page.extract_text() or ""
lines_with_y = [
{"y": i * 10, "text": line}
for i, line in enumerate(text_content.split("\n"))
]
# Extract text lines with Y positions. Use pdfplumber's line
# extraction rather than joining page.chars by hand: chars carry
# no separator between adjacent columns, so a join glued them
# together ("Customer NameVendor Name Ltd").
lines_with_y = [
{"y": line["top"], "text": line["text"].strip()}
for line in page.extract_text_lines(
strip=True, return_chars=False
)
]

# OCR all images
image_data = []
Expand Down
42 changes: 40 additions & 2 deletions packages/markitdown-ocr/tests/test_pdf_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ def test_pdf_complex_layout(svc: MockOCRService) -> None:
"## Page 1\n\n\n"
"Complex Layout Document\n\n"
"Table:\n\n"
"ItemQuantity\n\n\n\n"
"Item Quantity\n\n\n\n"
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n\n"
"Widget A5"
"Widget A 5"
)
assert _convert("pdf_complex_layout.pdf", svc) == expected

Expand Down Expand Up @@ -245,3 +245,41 @@ def test_pdf_no_ocr_service_no_tags() -> None:
md = converter.convert(f, StreamInfo(extension=".pdf")).text_content
assert "*[Image OCR]" not in md
assert "[End OCR]*" not in md


# ---------------------------------------------------------------------------
# Native text on a page that also has an embedded image
# ---------------------------------------------------------------------------


def _two_column_page_with_image() -> bytes:
"""One native-text page: two columns sharing a baseline, plus a small image."""
import fitz # PyMuPDF is a dependency of this package

doc = fitz.open()
page = doc.new_page(width=595, height=842)
page.insert_text((50, 100), "Customer Name", fontsize=11)
page.insert_text((350, 100), "Vendor Name Ltd", fontsize=11)
page.insert_text((50, 130), "Unit price", fontsize=11)
page.insert_text((350, 130), "Total price", fontsize=11)
pix = fitz.Pixmap(fitz.csRGB, fitz.IRect(0, 0, 60, 40), False)
pix.set_rect(pix.irect, (200, 30, 30))
page.insert_image(fitz.Rect(50, 30, 110, 70), stream=pix.tobytes("png"))
return doc.tobytes()


def test_pdf_page_with_image_keeps_spaces_between_columns(svc: MockOCRService) -> None:
"""Regression: on a page with an embedded image (e.g. a logo) the text used to be
rebuilt from raw chars joined with "", gluing adjacent columns together
("Customer NameVendor Name Ltd")."""
converter = PdfConverterWithOCR()
md = converter.convert(
io.BytesIO(_two_column_page_with_image()),
StreamInfo(extension=".pdf"),
ocr_service=svc,
).text_content

assert "Customer Name Vendor Name Ltd" in md
assert "Unit price Total price" in md
# the image is still OCR'd and interleaved
assert _OCR_BLOCK in md