markitdown-ocr 0.1.0__tar.gz → 0.1.1b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/PKG-INFO +2 -2
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/__about__.py +1 -1
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_docx_converter_with_ocr.py +47 -8
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_pptx_converter_with_ocr.py +5 -1
- markitdown_ocr-0.1.1b2/tests/test_docx_converter.py +428 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/test_pdf_converter.py +19 -6
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/test_pptx_converter.py +53 -0
- markitdown_ocr-0.1.0/tests/test_docx_converter.py +0 -223
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/.gitignore +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/LICENSE +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/README.md +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/pyproject.toml +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/__init__.py +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_ocr_service.py +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_pdf_converter_with_ocr.py +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_plugin.py +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_xlsx_converter_with_ocr.py +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/__init__.py +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/docx_complex_layout.docx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/docx_image_end.docx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/docx_image_middle.docx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/docx_image_start.docx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/docx_multipage.docx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/docx_multiple_images.docx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_complex_layout.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_image_end.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_image_middle.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_image_start.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_multipage.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_multiple_images.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_scanned_invoice.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_scanned_meeting_minutes.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_scanned_minimal.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_scanned_report.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_scanned_sales_report.pdf +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pptx_complex_layout.pptx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pptx_image_end.pptx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pptx_image_middle.pptx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pptx_image_start.pptx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pptx_multiple_images.pptx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/xlsx_complex_layout.xlsx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/xlsx_image_end.xlsx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/xlsx_image_middle.xlsx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/xlsx_image_start.xlsx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/xlsx_multiple_images.xlsx +0 -0
- {markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/test_xlsx_converter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: markitdown-ocr
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1b2
|
|
4
4
|
Summary: OCR plugin for MarkItDown - Extracts text from images in PDF, DOCX, PPTX, and XLSX via LLM Vision
|
|
5
5
|
Project-URL: Documentation, https://github.com/microsoft/markitdown#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/microsoft/markitdown/issues
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_docx_converter_with_ocr.py
RENAMED
|
@@ -29,6 +29,18 @@ except ImportError:
|
|
|
29
29
|
# Must be a single token with no special markdown characters.
|
|
30
30
|
_PLACEHOLDER = "MARKITDOWNOCRBLOCK{}"
|
|
31
31
|
|
|
32
|
+
_UNDERLINE_STYLE_MAP = "u => u"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_embedded_style_map(file_stream: BinaryIO) -> Optional[str]:
|
|
36
|
+
"""Read the style map embedded in a .docx, if it has one."""
|
|
37
|
+
position = file_stream.tell()
|
|
38
|
+
file_stream.seek(0)
|
|
39
|
+
try:
|
|
40
|
+
return mammoth.read_embedded_style_map(file_stream)
|
|
41
|
+
finally:
|
|
42
|
+
file_stream.seek(position)
|
|
43
|
+
|
|
32
44
|
|
|
33
45
|
class DocxConverterWithOCR(HtmlConverter):
|
|
34
46
|
"""
|
|
@@ -82,16 +94,40 @@ class DocxConverterWithOCR(HtmlConverter):
|
|
|
82
94
|
kwargs.get("ocr_service") or self.ocr_service
|
|
83
95
|
)
|
|
84
96
|
|
|
97
|
+
# Pre-process once, up front, so that every subsequent read sees the
|
|
98
|
+
# repaired archive. pre_process_docx() also fixes .docx files whose ZIP
|
|
99
|
+
# local file headers disagree with the central directory; reading the
|
|
100
|
+
# original stream first would make those raise, or silently yield no
|
|
101
|
+
# images and drop the OCR output.
|
|
102
|
+
pre_process_stream = pre_process_docx(file_stream)
|
|
103
|
+
|
|
104
|
+
# Read the embedded style map and combine with any provided style map
|
|
105
|
+
caller_style_map = kwargs.get("style_map")
|
|
106
|
+
embedded_style_map = _read_embedded_style_map(pre_process_stream)
|
|
107
|
+
|
|
108
|
+
style_map = "\n".join(
|
|
109
|
+
part
|
|
110
|
+
for part in (
|
|
111
|
+
caller_style_map,
|
|
112
|
+
embedded_style_map,
|
|
113
|
+
_UNDERLINE_STYLE_MAP,
|
|
114
|
+
)
|
|
115
|
+
if part
|
|
116
|
+
)
|
|
117
|
+
|
|
85
118
|
if ocr_service:
|
|
86
119
|
# 1. Extract and OCR images — returns raw text per image
|
|
87
|
-
|
|
88
|
-
image_ocr_map = self._extract_and_ocr_images(
|
|
120
|
+
pre_process_stream.seek(0)
|
|
121
|
+
image_ocr_map = self._extract_and_ocr_images(
|
|
122
|
+
pre_process_stream, ocr_service
|
|
123
|
+
)
|
|
89
124
|
|
|
90
125
|
# 2. Convert DOCX → HTML via mammoth
|
|
91
|
-
|
|
92
|
-
pre_process_stream = pre_process_docx(file_stream)
|
|
126
|
+
pre_process_stream.seek(0)
|
|
93
127
|
html_result = mammoth.convert_to_html(
|
|
94
|
-
pre_process_stream,
|
|
128
|
+
pre_process_stream,
|
|
129
|
+
style_map=style_map,
|
|
130
|
+
include_embedded_style_map=False,
|
|
95
131
|
).value
|
|
96
132
|
|
|
97
133
|
# 3. Replace <img> tags with plain placeholder tokens so that
|
|
@@ -116,10 +152,13 @@ class DocxConverterWithOCR(HtmlConverter):
|
|
|
116
152
|
return DocumentConverterResult(markdown=md)
|
|
117
153
|
else:
|
|
118
154
|
# Standard conversion without OCR
|
|
119
|
-
|
|
120
|
-
pre_process_stream = pre_process_docx(file_stream)
|
|
155
|
+
pre_process_stream.seek(0)
|
|
121
156
|
return self._html_converter.convert_string(
|
|
122
|
-
mammoth.convert_to_html(
|
|
157
|
+
mammoth.convert_to_html(
|
|
158
|
+
pre_process_stream,
|
|
159
|
+
style_map=style_map,
|
|
160
|
+
include_embedded_style_map=False,
|
|
161
|
+
).value,
|
|
123
162
|
**kwargs,
|
|
124
163
|
)
|
|
125
164
|
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_pptx_converter_with_ocr.py
RENAMED
|
@@ -222,7 +222,11 @@ class PptxConverterWithOCR(DocumentConverter):
|
|
|
222
222
|
def _convert_chart_to_markdown(self, chart):
|
|
223
223
|
try:
|
|
224
224
|
md = "\\n\\n### Chart"
|
|
225
|
-
|
|
225
|
+
# ChartTitle.text_frame is documented as destructive -- it creates
|
|
226
|
+
# a text frame if one isn't already present, so it never returns
|
|
227
|
+
# None. has_text_frame is the property that actually reflects
|
|
228
|
+
# whether a text frame exists.
|
|
229
|
+
if chart.has_title and chart.chart_title.has_text_frame:
|
|
226
230
|
md += f": {chart.chart_title.text_frame.text}"
|
|
227
231
|
md += "\\n\\n"
|
|
228
232
|
data = []
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for DocxConverterWithOCR.
|
|
3
|
+
|
|
4
|
+
For each DOCX test file: convert with a mock OCR service then compare the
|
|
5
|
+
full output string against the expected snapshot.
|
|
6
|
+
|
|
7
|
+
OCR block format used by the converter:
|
|
8
|
+
*[Image OCR]
|
|
9
|
+
MOCK_OCR_TEXT_12345
|
|
10
|
+
[End OCR]*
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import io
|
|
14
|
+
import struct
|
|
15
|
+
import sys
|
|
16
|
+
import zipfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
23
|
+
|
|
24
|
+
from markitdown_ocr._ocr_service import OCRResult # noqa: E402
|
|
25
|
+
from markitdown_ocr._docx_converter_with_ocr import ( # noqa: E402
|
|
26
|
+
DocxConverterWithOCR,
|
|
27
|
+
)
|
|
28
|
+
from markitdown import StreamInfo # noqa: E402
|
|
29
|
+
|
|
30
|
+
TEST_DATA_DIR = Path(__file__).parent / "ocr_test_data"
|
|
31
|
+
|
|
32
|
+
_MOCK_TEXT = "MOCK_OCR_TEXT_12345"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MockOCRService:
|
|
36
|
+
def extract_text( # noqa: ANN101
|
|
37
|
+
self, image_stream: Any, **kwargs: Any
|
|
38
|
+
) -> OCRResult:
|
|
39
|
+
return OCRResult(text=_MOCK_TEXT, backend_used="mock")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture(scope="module")
|
|
43
|
+
def svc() -> MockOCRService:
|
|
44
|
+
return MockOCRService()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _convert(filename: str, ocr_service: MockOCRService) -> str:
|
|
48
|
+
path = TEST_DATA_DIR / filename
|
|
49
|
+
if not path.exists():
|
|
50
|
+
pytest.skip(f"Test file not found: {path}")
|
|
51
|
+
converter = DocxConverterWithOCR()
|
|
52
|
+
with open(path, "rb") as f:
|
|
53
|
+
return converter.convert(
|
|
54
|
+
f, StreamInfo(extension=".docx"), ocr_service=ocr_service
|
|
55
|
+
).text_content
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# docx_image_start.docx
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_docx_image_start(svc: MockOCRService) -> None:
|
|
64
|
+
expected = (
|
|
65
|
+
"Document with Image at Start\n\n"
|
|
66
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
67
|
+
"This is the main content after the header image.\n\n"
|
|
68
|
+
"More text content here."
|
|
69
|
+
)
|
|
70
|
+
assert _convert("docx_image_start.docx", svc) == expected
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# docx_image_middle.docx
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_docx_image_middle(svc: MockOCRService) -> None:
|
|
79
|
+
expected = (
|
|
80
|
+
"# Introduction\n\n"
|
|
81
|
+
"This is the introduction section.\n\n"
|
|
82
|
+
"We will see an image below.\n\n"
|
|
83
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
84
|
+
"# Analysis\n\n"
|
|
85
|
+
"This section comes after the image."
|
|
86
|
+
)
|
|
87
|
+
assert _convert("docx_image_middle.docx", svc) == expected
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# docx_image_end.docx
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_docx_image_end(svc: MockOCRService) -> None:
|
|
96
|
+
expected = (
|
|
97
|
+
"Report\n\n"
|
|
98
|
+
"Main findings of the report.\n\n"
|
|
99
|
+
"Details and analysis.\n\n"
|
|
100
|
+
"Recommendations.\n\n"
|
|
101
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*"
|
|
102
|
+
)
|
|
103
|
+
assert _convert("docx_image_end.docx", svc) == expected
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# docx_multiple_images.docx
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_docx_multiple_images(svc: MockOCRService) -> None:
|
|
112
|
+
expected = (
|
|
113
|
+
"Multi-Image Document\n\n"
|
|
114
|
+
"First section\n\n"
|
|
115
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
116
|
+
"Second section with another image\n\n"
|
|
117
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
118
|
+
"Conclusion"
|
|
119
|
+
)
|
|
120
|
+
assert _convert("docx_multiple_images.docx", svc) == expected
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# docx_multipage.docx
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_docx_multipage(svc: MockOCRService) -> None:
|
|
129
|
+
expected = (
|
|
130
|
+
"# Page 1 - Mixed Content\n\n"
|
|
131
|
+
"This is the first paragraph on page 1.\n\n"
|
|
132
|
+
"BEFORE IMAGE: Important content appears here.\n\n"
|
|
133
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
134
|
+
"AFTER IMAGE: This content follows the image.\n\n"
|
|
135
|
+
"More text on page 1.\n\n"
|
|
136
|
+
"# Page 2 - Image at End\n\n"
|
|
137
|
+
"Content on page 2.\n\n"
|
|
138
|
+
"Multiple paragraphs of text.\n\n"
|
|
139
|
+
"Building up to the image...\n\n"
|
|
140
|
+
"Final paragraph before image.\n\n"
|
|
141
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
142
|
+
"# Page 3 - Image at Start\n\n"
|
|
143
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
144
|
+
"Content that follows the header image.\n\n"
|
|
145
|
+
"AFTER IMAGE: This text is after the image."
|
|
146
|
+
)
|
|
147
|
+
assert _convert("docx_multipage.docx", svc) == expected
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# docx_complex_layout.docx
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_docx_complex_layout(svc: MockOCRService) -> None:
|
|
156
|
+
expected = (
|
|
157
|
+
"Complex Document\n\n"
|
|
158
|
+
"| | |\n"
|
|
159
|
+
"| --- | --- |\n"
|
|
160
|
+
"| Feature | Status |\n"
|
|
161
|
+
"| Authentication | Active |\n"
|
|
162
|
+
"| Encryption | Enabled |\n\n"
|
|
163
|
+
"Security notice:\n\n"
|
|
164
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*"
|
|
165
|
+
)
|
|
166
|
+
assert _convert("docx_complex_layout.docx", svc) == expected
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# _inject_placeholders — internal unit tests (no file I/O)
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_inject_placeholders_single_image() -> None:
|
|
175
|
+
converter = DocxConverterWithOCR()
|
|
176
|
+
html = "<p>Before</p><img src='x.png'/><p>After</p>"
|
|
177
|
+
result_html, texts = converter._inject_placeholders(html, {"rId1": "TEXT"})
|
|
178
|
+
assert "<img" not in result_html
|
|
179
|
+
assert "MARKITDOWNOCRBLOCK0" in result_html
|
|
180
|
+
assert texts == ["TEXT"]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_inject_placeholders_two_images_sequential_tokens() -> None:
|
|
184
|
+
converter = DocxConverterWithOCR()
|
|
185
|
+
html = "<img src='a.png'/><p>Mid</p><img src='b.png'/>"
|
|
186
|
+
result_html, texts = converter._inject_placeholders(
|
|
187
|
+
html, {"rId1": "FIRST", "rId2": "SECOND"}
|
|
188
|
+
)
|
|
189
|
+
assert "MARKITDOWNOCRBLOCK0" in result_html
|
|
190
|
+
assert "MARKITDOWNOCRBLOCK1" in result_html
|
|
191
|
+
assert result_html.index("MARKITDOWNOCRBLOCK0") < result_html.index(
|
|
192
|
+
"MARKITDOWNOCRBLOCK1"
|
|
193
|
+
)
|
|
194
|
+
assert len(texts) == 2
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_inject_placeholders_no_img_tag_appends_at_end() -> None:
|
|
198
|
+
converter = DocxConverterWithOCR()
|
|
199
|
+
html = "<p>No images</p>"
|
|
200
|
+
result_html, texts = converter._inject_placeholders(html, {"rId1": "ORPHAN"})
|
|
201
|
+
assert "MARKITDOWNOCRBLOCK0" in result_html
|
|
202
|
+
assert texts == ["ORPHAN"]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_inject_placeholders_empty_map_leaves_html_unchanged() -> None:
|
|
206
|
+
converter = DocxConverterWithOCR()
|
|
207
|
+
html = "<p>Content</p><img src='pic.jpg'/>"
|
|
208
|
+
result_html, texts = converter._inject_placeholders(html, {})
|
|
209
|
+
assert result_html == html
|
|
210
|
+
assert texts == []
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# No OCR service — no OCR tags emitted
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_docx_no_ocr_service_no_tags() -> None:
|
|
219
|
+
path = TEST_DATA_DIR / "docx_image_middle.docx"
|
|
220
|
+
if not path.exists():
|
|
221
|
+
pytest.skip(f"Test file not found: {path}")
|
|
222
|
+
converter = DocxConverterWithOCR()
|
|
223
|
+
with open(path, "rb") as f:
|
|
224
|
+
md = converter.convert(f, StreamInfo(extension=".docx")).text_content
|
|
225
|
+
assert "*[Image OCR]" not in md
|
|
226
|
+
assert "[End OCR]*" not in md
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@pytest.mark.parametrize("double_strike", [False, True])
|
|
230
|
+
@pytest.mark.parametrize("use_ocr", [False, True])
|
|
231
|
+
def test_docx_styles_with_redundant_default_namespace(
|
|
232
|
+
svc: MockOCRService, use_ocr: bool, double_strike: bool
|
|
233
|
+
) -> None:
|
|
234
|
+
path = TEST_DATA_DIR / "docx_image_middle.docx"
|
|
235
|
+
if not path.exists():
|
|
236
|
+
pytest.skip(f"Test file not found: {path}")
|
|
237
|
+
original = path.read_bytes()
|
|
238
|
+
fixture = io.BytesIO()
|
|
239
|
+
namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
240
|
+
declaration = f'xmlns:w="{namespace}"'.encode("utf-8")
|
|
241
|
+
with zipfile.ZipFile(io.BytesIO(original)) as source, zipfile.ZipFile(
|
|
242
|
+
fixture, "w"
|
|
243
|
+
) as target:
|
|
244
|
+
for item in source.infolist():
|
|
245
|
+
content = source.read(item)
|
|
246
|
+
if item.filename == "word/styles.xml":
|
|
247
|
+
if double_strike:
|
|
248
|
+
assert content.count(b"</w:styles>") == 1
|
|
249
|
+
content = content.replace(
|
|
250
|
+
b"</w:styles>",
|
|
251
|
+
b'<w:style w:type="character" w:styleId="DoubleStrike">'
|
|
252
|
+
b'<w:name w:val="Double Strike"/>'
|
|
253
|
+
b'<w:rPr><w:dstrike w:val="1"/></w:rPr>'
|
|
254
|
+
b"</w:style></w:styles>",
|
|
255
|
+
1,
|
|
256
|
+
)
|
|
257
|
+
assert content.count(declaration) == 1
|
|
258
|
+
content = content.replace(
|
|
259
|
+
declaration, declaration + f' xmlns="{namespace}"'.encode(), 1
|
|
260
|
+
)
|
|
261
|
+
target.writestr(item, content)
|
|
262
|
+
|
|
263
|
+
converter = DocxConverterWithOCR()
|
|
264
|
+
service = svc if use_ocr else None
|
|
265
|
+
expected = converter.convert(
|
|
266
|
+
io.BytesIO(original), StreamInfo(extension=".docx"), ocr_service=service
|
|
267
|
+
).markdown
|
|
268
|
+
fixture.seek(0)
|
|
269
|
+
actual = converter.convert(
|
|
270
|
+
fixture, StreamInfo(extension=".docx"), ocr_service=service
|
|
271
|
+
).markdown
|
|
272
|
+
|
|
273
|
+
assert "# Introduction" in actual
|
|
274
|
+
assert actual == expected
|
|
275
|
+
if use_ocr:
|
|
276
|
+
assert _MOCK_TEXT in actual
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Underlined runs survive both the OCR and the non-OCR mammoth paths
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _underlined_docx(
|
|
285
|
+
tmp_path: Path,
|
|
286
|
+
*,
|
|
287
|
+
paragraph_xml: str = (
|
|
288
|
+
"<w:r><w:t>plain </w:t></w:r>"
|
|
289
|
+
'<w:r><w:rPr><w:u w:val="single"/></w:rPr><w:t>underlined</w:t></w:r>'
|
|
290
|
+
),
|
|
291
|
+
) -> Path:
|
|
292
|
+
docx_file = tmp_path / "underlined.docx"
|
|
293
|
+
document_xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
294
|
+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
295
|
+
<w:body>
|
|
296
|
+
<w:p>{paragraph_xml}</w:p>
|
|
297
|
+
</w:body>
|
|
298
|
+
</w:document>"""
|
|
299
|
+
|
|
300
|
+
with zipfile.ZipFile(docx_file, "w") as archive:
|
|
301
|
+
archive.writestr(
|
|
302
|
+
"[Content_Types].xml",
|
|
303
|
+
"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
304
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
305
|
+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
306
|
+
<Default Extension="xml" ContentType="application/xml"/>
|
|
307
|
+
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
|
308
|
+
</Types>""",
|
|
309
|
+
)
|
|
310
|
+
archive.writestr(
|
|
311
|
+
"_rels/.rels",
|
|
312
|
+
"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
313
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
314
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
|
315
|
+
</Relationships>""",
|
|
316
|
+
)
|
|
317
|
+
archive.writestr("word/document.xml", document_xml)
|
|
318
|
+
|
|
319
|
+
return docx_file
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_docx_underlined_text_is_preserved_without_ocr(tmp_path: Path) -> None:
|
|
323
|
+
converter = DocxConverterWithOCR()
|
|
324
|
+
with open(_underlined_docx(tmp_path), "rb") as f:
|
|
325
|
+
md = converter.convert(f, StreamInfo(extension=".docx")).markdown
|
|
326
|
+
assert "plain <u>underlined</u>" in md
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_docx_underlined_text_is_preserved_with_ocr(
|
|
330
|
+
tmp_path: Path, svc: MockOCRService
|
|
331
|
+
) -> None:
|
|
332
|
+
converter = DocxConverterWithOCR()
|
|
333
|
+
with open(_underlined_docx(tmp_path), "rb") as f:
|
|
334
|
+
md = converter.convert(
|
|
335
|
+
f, StreamInfo(extension=".docx"), ocr_service=svc
|
|
336
|
+
).markdown
|
|
337
|
+
assert "plain <u>underlined</u>" in md
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@pytest.mark.parametrize("use_ocr", [False, True])
|
|
341
|
+
@pytest.mark.parametrize(
|
|
342
|
+
("run_xml", "expected"),
|
|
343
|
+
[
|
|
344
|
+
('<w:t xml:space="preserve"> </w:t>', "First Last"),
|
|
345
|
+
("<w:tab/>", "First Last"),
|
|
346
|
+
("<w:t> </w:t>", "First\u00a0Last"),
|
|
347
|
+
# Direct conversion keeps the two-space hard break; the dispatcher strips it.
|
|
348
|
+
("<w:br/>", "First \nLast"),
|
|
349
|
+
],
|
|
350
|
+
)
|
|
351
|
+
def test_docx_underlined_whitespace_is_preserved(
|
|
352
|
+
tmp_path: Path,
|
|
353
|
+
svc: MockOCRService,
|
|
354
|
+
use_ocr: bool,
|
|
355
|
+
run_xml: str,
|
|
356
|
+
expected: str,
|
|
357
|
+
) -> None:
|
|
358
|
+
path = _underlined_docx(
|
|
359
|
+
tmp_path,
|
|
360
|
+
paragraph_xml=(
|
|
361
|
+
"<w:r><w:t>First</w:t></w:r>"
|
|
362
|
+
f'<w:r><w:rPr><w:u w:val="single"/></w:rPr>{run_xml}</w:r>'
|
|
363
|
+
"<w:r><w:t>Last</w:t></w:r>"
|
|
364
|
+
),
|
|
365
|
+
)
|
|
366
|
+
converter = DocxConverterWithOCR()
|
|
367
|
+
with path.open("rb") as stream:
|
|
368
|
+
result = converter.convert(
|
|
369
|
+
stream, StreamInfo(extension=".docx"), ocr_service=svc if use_ocr else None
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
assert result.markdown == expected
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
# ZIP local file header casing mismatch
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _uppercase_local_header_names(docx_bytes: bytes) -> bytes:
|
|
381
|
+
"""Uppercase every local file header filename, leaving the central directory
|
|
382
|
+
untouched, to mimic generators that disagree with themselves about casing."""
|
|
383
|
+
raw = bytearray(docx_bytes)
|
|
384
|
+
offset = patched = 0
|
|
385
|
+
while offset + 30 <= len(raw) and raw[offset : offset + 4] == b"PK\x03\x04":
|
|
386
|
+
fname_len = struct.unpack_from("<H", raw, offset + 26)[0]
|
|
387
|
+
extra_len = struct.unpack_from("<H", raw, offset + 28)[0]
|
|
388
|
+
comp_size = struct.unpack_from("<I", raw, offset + 18)[0]
|
|
389
|
+
name = raw[offset + 30 : offset + 30 + fname_len]
|
|
390
|
+
if name.upper() != name:
|
|
391
|
+
raw[offset + 30 : offset + 30 + fname_len] = name.upper()
|
|
392
|
+
patched += 1
|
|
393
|
+
offset += 30 + fname_len + extra_len + comp_size
|
|
394
|
+
assert patched, "fixture produced no mismatched local file headers"
|
|
395
|
+
return bytes(raw)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_docx_zip_filename_casing_mismatch_preserves_ocr(svc: MockOCRService) -> None:
|
|
399
|
+
"""OCR output survives a .docx whose local file headers disagree with the
|
|
400
|
+
central directory on casing.
|
|
401
|
+
|
|
402
|
+
The image extraction swallows every exception, so an unrepaired stream used
|
|
403
|
+
to yield an empty OCR map and silently drop the OCR blocks rather than fail.
|
|
404
|
+
"""
|
|
405
|
+
path = TEST_DATA_DIR / "docx_image_middle.docx"
|
|
406
|
+
if not path.exists():
|
|
407
|
+
pytest.skip(f"Test file not found: {path}")
|
|
408
|
+
|
|
409
|
+
original = path.read_bytes()
|
|
410
|
+
mismatched = _uppercase_local_header_names(original)
|
|
411
|
+
|
|
412
|
+
# Plain zipfile cannot read the mismatched archive ...
|
|
413
|
+
with pytest.raises(zipfile.BadZipFile):
|
|
414
|
+
with zipfile.ZipFile(io.BytesIO(mismatched), "r") as zf:
|
|
415
|
+
for name in zf.namelist():
|
|
416
|
+
zf.read(name)
|
|
417
|
+
|
|
418
|
+
# ... but the converter produces the same output it does for the original.
|
|
419
|
+
converter = DocxConverterWithOCR()
|
|
420
|
+
expected = converter.convert(
|
|
421
|
+
io.BytesIO(original), StreamInfo(extension=".docx"), ocr_service=svc
|
|
422
|
+
).markdown
|
|
423
|
+
actual = converter.convert(
|
|
424
|
+
io.BytesIO(mismatched), StreamInfo(extension=".docx"), ocr_service=svc
|
|
425
|
+
).markdown
|
|
426
|
+
|
|
427
|
+
assert _MOCK_TEXT in actual
|
|
428
|
+
assert actual == expected
|
|
@@ -144,17 +144,30 @@ def test_pdf_complex_layout(svc: MockOCRService) -> None:
|
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
# ---------------------------------------------------------------------------
|
|
147
|
-
# pdf_multipage.pdf
|
|
147
|
+
# pdf_multipage.pdf
|
|
148
148
|
# ---------------------------------------------------------------------------
|
|
149
149
|
|
|
150
150
|
|
|
151
151
|
def test_pdf_multipage(svc: MockOCRService) -> None:
|
|
152
|
-
# pdfplumber cannot open this file (Unexpected EOF), so _ocr_full_pages
|
|
153
|
-
# falls back to PyMuPDF for page rendering. Each page becomes one OCR block.
|
|
154
152
|
expected = (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
"## Page 1\n\n\n"
|
|
154
|
+
"Page 1 - Content before image\n\n"
|
|
155
|
+
"This is important text that appears BEFORE the image.\n\n\n\n"
|
|
156
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n\n"
|
|
157
|
+
"This text appears AFTER the image on page 1.\n\n"
|
|
158
|
+
"More content follows here.\n\n\n"
|
|
159
|
+
"## Page 2\n\n\n"
|
|
160
|
+
"Page 2 - Content with image at end\n\n"
|
|
161
|
+
"Main content of page 2 starts here.\n\n"
|
|
162
|
+
"This is paragraph 1.\n\n"
|
|
163
|
+
"This is paragraph 2.\n\n"
|
|
164
|
+
"Final paragraph before image.\n\n\n\n"
|
|
165
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n\n\n"
|
|
166
|
+
"## Page 3\n\n\n"
|
|
167
|
+
"Page 3 - Image at top\n\n\n\n"
|
|
168
|
+
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n\n"
|
|
169
|
+
"Content that follows the image.\n\n"
|
|
170
|
+
"This text is AFTER the image."
|
|
158
171
|
)
|
|
159
172
|
assert _convert("pdf_multipage.pdf", svc) == expected
|
|
160
173
|
|
|
@@ -146,3 +146,56 @@ def test_pptx_no_ocr_service_no_tags() -> None:
|
|
|
146
146
|
md = converter.convert(f, StreamInfo(extension=".pptx")).text_content
|
|
147
147
|
assert "*[Image OCR]" not in md
|
|
148
148
|
assert "[End OCR]*" not in md
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_pptx_ocr_chart_no_title_text_frame() -> None:
|
|
152
|
+
from unittest.mock import MagicMock
|
|
153
|
+
from markitdown_ocr._pptx_converter_with_ocr import PptxConverterWithOCR
|
|
154
|
+
|
|
155
|
+
# python-pptx's ChartTitle.text_frame is destructive -- it creates a text
|
|
156
|
+
# frame if one isn't already present, so it never returns None.
|
|
157
|
+
# has_text_frame is the property that actually reflects presence/absence.
|
|
158
|
+
mock_chart = MagicMock()
|
|
159
|
+
mock_chart.has_title = True
|
|
160
|
+
mock_chart.chart_title.has_text_frame = False
|
|
161
|
+
|
|
162
|
+
mock_category = MagicMock()
|
|
163
|
+
mock_category.label = "Cat 1"
|
|
164
|
+
mock_chart.plots = [MagicMock(categories=[mock_category])]
|
|
165
|
+
|
|
166
|
+
mock_series = MagicMock()
|
|
167
|
+
mock_series.name = "Series 1"
|
|
168
|
+
mock_series.values = [10.0]
|
|
169
|
+
mock_chart.series = [mock_series]
|
|
170
|
+
|
|
171
|
+
converter = PptxConverterWithOCR()
|
|
172
|
+
result = converter._convert_chart_to_markdown(mock_chart)
|
|
173
|
+
|
|
174
|
+
assert "### Chart" in result
|
|
175
|
+
assert "Cat 1" in result
|
|
176
|
+
assert "Series 1" in result
|
|
177
|
+
assert ":" not in result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_pptx_ocr_chart_with_title_text_frame() -> None:
|
|
181
|
+
from unittest.mock import MagicMock
|
|
182
|
+
from markitdown_ocr._pptx_converter_with_ocr import PptxConverterWithOCR
|
|
183
|
+
|
|
184
|
+
mock_chart = MagicMock()
|
|
185
|
+
mock_chart.has_title = True
|
|
186
|
+
mock_chart.chart_title.has_text_frame = True
|
|
187
|
+
mock_chart.chart_title.text_frame.text = "Revenue"
|
|
188
|
+
|
|
189
|
+
mock_category = MagicMock()
|
|
190
|
+
mock_category.label = "Cat 1"
|
|
191
|
+
mock_chart.plots = [MagicMock(categories=[mock_category])]
|
|
192
|
+
|
|
193
|
+
mock_series = MagicMock()
|
|
194
|
+
mock_series.name = "Series 1"
|
|
195
|
+
mock_series.values = [10.0]
|
|
196
|
+
mock_chart.series = [mock_series]
|
|
197
|
+
|
|
198
|
+
converter = PptxConverterWithOCR()
|
|
199
|
+
result = converter._convert_chart_to_markdown(mock_chart)
|
|
200
|
+
|
|
201
|
+
assert "### Chart: Revenue" in result
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unit tests for DocxConverterWithOCR.
|
|
3
|
-
|
|
4
|
-
For each DOCX test file: convert with a mock OCR service then compare the
|
|
5
|
-
full output string against the expected snapshot.
|
|
6
|
-
|
|
7
|
-
OCR block format used by the converter:
|
|
8
|
-
*[Image OCR]
|
|
9
|
-
MOCK_OCR_TEXT_12345
|
|
10
|
-
[End OCR]*
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
|
-
import pytest
|
|
18
|
-
|
|
19
|
-
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
20
|
-
|
|
21
|
-
from markitdown_ocr._ocr_service import OCRResult # noqa: E402
|
|
22
|
-
from markitdown_ocr._docx_converter_with_ocr import ( # noqa: E402
|
|
23
|
-
DocxConverterWithOCR,
|
|
24
|
-
)
|
|
25
|
-
from markitdown import StreamInfo # noqa: E402
|
|
26
|
-
|
|
27
|
-
TEST_DATA_DIR = Path(__file__).parent / "ocr_test_data"
|
|
28
|
-
|
|
29
|
-
_MOCK_TEXT = "MOCK_OCR_TEXT_12345"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class MockOCRService:
|
|
33
|
-
def extract_text( # noqa: ANN101
|
|
34
|
-
self, image_stream: Any, **kwargs: Any
|
|
35
|
-
) -> OCRResult:
|
|
36
|
-
return OCRResult(text=_MOCK_TEXT, backend_used="mock")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@pytest.fixture(scope="module")
|
|
40
|
-
def svc() -> MockOCRService:
|
|
41
|
-
return MockOCRService()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _convert(filename: str, ocr_service: MockOCRService) -> str:
|
|
45
|
-
path = TEST_DATA_DIR / filename
|
|
46
|
-
if not path.exists():
|
|
47
|
-
pytest.skip(f"Test file not found: {path}")
|
|
48
|
-
converter = DocxConverterWithOCR()
|
|
49
|
-
with open(path, "rb") as f:
|
|
50
|
-
return converter.convert(
|
|
51
|
-
f, StreamInfo(extension=".docx"), ocr_service=ocr_service
|
|
52
|
-
).text_content
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# ---------------------------------------------------------------------------
|
|
56
|
-
# docx_image_start.docx
|
|
57
|
-
# ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def test_docx_image_start(svc: MockOCRService) -> None:
|
|
61
|
-
expected = (
|
|
62
|
-
"Document with Image at Start\n\n"
|
|
63
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
64
|
-
"This is the main content after the header image.\n\n"
|
|
65
|
-
"More text content here."
|
|
66
|
-
)
|
|
67
|
-
assert _convert("docx_image_start.docx", svc) == expected
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# ---------------------------------------------------------------------------
|
|
71
|
-
# docx_image_middle.docx
|
|
72
|
-
# ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def test_docx_image_middle(svc: MockOCRService) -> None:
|
|
76
|
-
expected = (
|
|
77
|
-
"# Introduction\n\n"
|
|
78
|
-
"This is the introduction section.\n\n"
|
|
79
|
-
"We will see an image below.\n\n"
|
|
80
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
81
|
-
"# Analysis\n\n"
|
|
82
|
-
"This section comes after the image."
|
|
83
|
-
)
|
|
84
|
-
assert _convert("docx_image_middle.docx", svc) == expected
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# ---------------------------------------------------------------------------
|
|
88
|
-
# docx_image_end.docx
|
|
89
|
-
# ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_docx_image_end(svc: MockOCRService) -> None:
|
|
93
|
-
expected = (
|
|
94
|
-
"Report\n\n"
|
|
95
|
-
"Main findings of the report.\n\n"
|
|
96
|
-
"Details and analysis.\n\n"
|
|
97
|
-
"Recommendations.\n\n"
|
|
98
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*"
|
|
99
|
-
)
|
|
100
|
-
assert _convert("docx_image_end.docx", svc) == expected
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# ---------------------------------------------------------------------------
|
|
104
|
-
# docx_multiple_images.docx
|
|
105
|
-
# ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def test_docx_multiple_images(svc: MockOCRService) -> None:
|
|
109
|
-
expected = (
|
|
110
|
-
"Multi-Image Document\n\n"
|
|
111
|
-
"First section\n\n"
|
|
112
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
113
|
-
"Second section with another image\n\n"
|
|
114
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
115
|
-
"Conclusion"
|
|
116
|
-
)
|
|
117
|
-
assert _convert("docx_multiple_images.docx", svc) == expected
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# ---------------------------------------------------------------------------
|
|
121
|
-
# docx_multipage.docx
|
|
122
|
-
# ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def test_docx_multipage(svc: MockOCRService) -> None:
|
|
126
|
-
expected = (
|
|
127
|
-
"# Page 1 - Mixed Content\n\n"
|
|
128
|
-
"This is the first paragraph on page 1.\n\n"
|
|
129
|
-
"BEFORE IMAGE: Important content appears here.\n\n"
|
|
130
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
131
|
-
"AFTER IMAGE: This content follows the image.\n\n"
|
|
132
|
-
"More text on page 1.\n\n"
|
|
133
|
-
"# Page 2 - Image at End\n\n"
|
|
134
|
-
"Content on page 2.\n\n"
|
|
135
|
-
"Multiple paragraphs of text.\n\n"
|
|
136
|
-
"Building up to the image...\n\n"
|
|
137
|
-
"Final paragraph before image.\n\n"
|
|
138
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
139
|
-
"# Page 3 - Image at Start\n\n"
|
|
140
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*\n\n"
|
|
141
|
-
"Content that follows the header image.\n\n"
|
|
142
|
-
"AFTER IMAGE: This text is after the image."
|
|
143
|
-
)
|
|
144
|
-
assert _convert("docx_multipage.docx", svc) == expected
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# ---------------------------------------------------------------------------
|
|
148
|
-
# docx_complex_layout.docx
|
|
149
|
-
# ---------------------------------------------------------------------------
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def test_docx_complex_layout(svc: MockOCRService) -> None:
|
|
153
|
-
expected = (
|
|
154
|
-
"Complex Document\n\n"
|
|
155
|
-
"| | |\n"
|
|
156
|
-
"| --- | --- |\n"
|
|
157
|
-
"| Feature | Status |\n"
|
|
158
|
-
"| Authentication | Active |\n"
|
|
159
|
-
"| Encryption | Enabled |\n\n"
|
|
160
|
-
"Security notice:\n\n"
|
|
161
|
-
"*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*"
|
|
162
|
-
)
|
|
163
|
-
assert _convert("docx_complex_layout.docx", svc) == expected
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
# ---------------------------------------------------------------------------
|
|
167
|
-
# _inject_placeholders — internal unit tests (no file I/O)
|
|
168
|
-
# ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def test_inject_placeholders_single_image() -> None:
|
|
172
|
-
converter = DocxConverterWithOCR()
|
|
173
|
-
html = "<p>Before</p><img src='x.png'/><p>After</p>"
|
|
174
|
-
result_html, texts = converter._inject_placeholders(html, {"rId1": "TEXT"})
|
|
175
|
-
assert "<img" not in result_html
|
|
176
|
-
assert "MARKITDOWNOCRBLOCK0" in result_html
|
|
177
|
-
assert texts == ["TEXT"]
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def test_inject_placeholders_two_images_sequential_tokens() -> None:
|
|
181
|
-
converter = DocxConverterWithOCR()
|
|
182
|
-
html = "<img src='a.png'/><p>Mid</p><img src='b.png'/>"
|
|
183
|
-
result_html, texts = converter._inject_placeholders(
|
|
184
|
-
html, {"rId1": "FIRST", "rId2": "SECOND"}
|
|
185
|
-
)
|
|
186
|
-
assert "MARKITDOWNOCRBLOCK0" in result_html
|
|
187
|
-
assert "MARKITDOWNOCRBLOCK1" in result_html
|
|
188
|
-
assert result_html.index("MARKITDOWNOCRBLOCK0") < result_html.index(
|
|
189
|
-
"MARKITDOWNOCRBLOCK1"
|
|
190
|
-
)
|
|
191
|
-
assert len(texts) == 2
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def test_inject_placeholders_no_img_tag_appends_at_end() -> None:
|
|
195
|
-
converter = DocxConverterWithOCR()
|
|
196
|
-
html = "<p>No images</p>"
|
|
197
|
-
result_html, texts = converter._inject_placeholders(html, {"rId1": "ORPHAN"})
|
|
198
|
-
assert "MARKITDOWNOCRBLOCK0" in result_html
|
|
199
|
-
assert texts == ["ORPHAN"]
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def test_inject_placeholders_empty_map_leaves_html_unchanged() -> None:
|
|
203
|
-
converter = DocxConverterWithOCR()
|
|
204
|
-
html = "<p>Content</p><img src='pic.jpg'/>"
|
|
205
|
-
result_html, texts = converter._inject_placeholders(html, {})
|
|
206
|
-
assert result_html == html
|
|
207
|
-
assert texts == []
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# ---------------------------------------------------------------------------
|
|
211
|
-
# No OCR service — no OCR tags emitted
|
|
212
|
-
# ---------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def test_docx_no_ocr_service_no_tags() -> None:
|
|
216
|
-
path = TEST_DATA_DIR / "docx_image_middle.docx"
|
|
217
|
-
if not path.exists():
|
|
218
|
-
pytest.skip(f"Test file not found: {path}")
|
|
219
|
-
converter = DocxConverterWithOCR()
|
|
220
|
-
with open(path, "rb") as f:
|
|
221
|
-
md = converter.convert(f, StreamInfo(extension=".docx")).text_content
|
|
222
|
-
assert "*[Image OCR]" not in md
|
|
223
|
-
assert "[End OCR]*" not in md
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_pdf_converter_with_ocr.py
RENAMED
|
File without changes
|
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/src/markitdown_ocr/_xlsx_converter_with_ocr.py
RENAMED
|
File without changes
|
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/docx_complex_layout.docx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/docx_multiple_images.docx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_scanned_meeting_minutes.pdf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pdf_scanned_sales_report.pdf
RENAMED
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pptx_complex_layout.pptx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/pptx_multiple_images.pptx
RENAMED
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/xlsx_complex_layout.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markitdown_ocr-0.1.0 → markitdown_ocr-0.1.1b2}/tests/ocr_test_data/xlsx_multiple_images.xlsx
RENAMED
|
File without changes
|
|
File without changes
|