Skip to content

Commit 508d2e1

Browse files
authored
improve ocr
1 parent b3cd2d4 commit 508d2e1

2 files changed

Lines changed: 80 additions & 8 deletions

File tree

src/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
"nltk==3.9.1", # not higher; gives unexplained error
152152
"numba==0.61.0", # only required by openai-whisper
153153
"numpy==1.26.4", # langchain libraries <2; numba <2.1; scipy <2.3; chattts <2.0.0
154-
"ocrmypdf==16.9.0",
154+
"ocrmypdf==16.10.0",
155155
"olefile==0.47",
156156
"openai==1.65.2", # only required by chat_lm_studio.py script and whispers2t (if using openai vanilla backend)
157157
"openai-whisper==20240930", # only required by whisper_s2t (if using openai vanilla backend)

src/module_ocr.py

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,31 @@
1111
from typing import Union, List, Dict, Tuple
1212
from multiprocessing import Process, Queue, Value
1313

14+
############################
15+
# MONKEY PATCH BEGINNING
16+
############################
17+
# REMOVE MONKEY PATCH CODE IF THIS PR IS ACCEPTED: https://github.com/ocrmypdf/OCRmyPDF/pull/1493
18+
from ocrmypdf.hocrtransform import HocrTransform
19+
from pikepdf.canvas import TextDirection
20+
21+
original_get_text_direction = HocrTransform._get_text_direction
22+
23+
def fixed_get_text_direction(self, par):
24+
"""Get the text direction of the paragraph with None check."""
25+
if par is None:
26+
return TextDirection.LTR # Default to left-to-right
27+
28+
return (
29+
TextDirection.RTL
30+
if par.attrib.get('dir', 'ltr') == 'rtl'
31+
else TextDirection.LTR
32+
)
33+
34+
HocrTransform._get_text_direction = fixed_get_text_direction
35+
############################
36+
# MONKEY PATCH ENDING
37+
############################
38+
1439
class OCRProcessor(ABC):
1540
def __init__(self, zoom: int = 2, progress_queue: Queue = None):
1641
self.zoom = zoom
@@ -33,7 +58,7 @@ def convert_page_to_image(self, page) -> Image.Image:
3358
def process_page(self, page_num: int, pdf_path: str) -> Tuple[int, str]:
3459
"""
3560
Process a single page from a PDF file.
36-
Each implementation should handle opening and closing the PDF.
61+
Each OCR backend should handle opening and closing the PDF.
3762
"""
3863
pass
3964

@@ -113,8 +138,7 @@ def initialize(self):
113138

114139
script_dir = Path(__file__).resolve().parent
115140

116-
# control temporary directory
117-
# necessary in case the default temp locations don't have write permission
141+
# specify temp dir since sometimes default locations don't have write permission
118142
self.temp_dir = script_dir / "temp_ocr"
119143
self.temp_dir.mkdir(exist_ok=True)
120144

@@ -168,7 +192,8 @@ def process_document(self, pdf_path: Path, output_path: Path = None):
168192
output_pdf.save(output_path)
169193
output_pdf.close()
170194

171-
# final cleanup
195+
self.optimize_final_pdf(pdf_path, output_path)
196+
172197
self.cleanup_temp_pdfs()
173198

174199
if self.progress_queue:
@@ -198,12 +223,24 @@ def process_page(self, page_num: int, pdf_path: str) -> Tuple[int, str]:
198223
pil_image = Image.open(BytesIO(pix.tobytes("png")))
199224

200225
api.SetImage(pil_image)
226+
201227
hocr_text = api.GetHOCRText(0)
202228

203-
with tempfile.NamedTemporaryFile(delete=False, suffix=".hocr", dir=self.temp_dir) as hocr_temp:
204-
hocr_output = hocr_temp.name
229+
# # DEBUG
230+
# if not hocr_text.strip():
231+
# print(f"Warning: No text detected on page {page_num}. Skipping HOCR file creation.")
232+
# else:
233+
# hocr_output = f"{self.temp_dir}/page_{page_num}.hocr"
234+
# Path(hocr_output).write_text(hocr_text, encoding="utf-8")
235+
# file_size = Path(hocr_output).stat().st_size
236+
# print(f"HOCR file saved: {hocr_output} (Size: {file_size} bytes)")
237+
238+
# name the hocr file by page number of the pdf
239+
hocr_output = f"{self.temp_dir}/page_{page_num}.hocr"
205240
Path(hocr_output).write_text(hocr_text, encoding="utf-8")
206241

242+
# print(f"Processing page {page_num}, HOCR file saved: {hocr_output}") # DEBUG
243+
207244
fd, text_pdf = tempfile.mkstemp(suffix=".pdf", dir=self.temp_dir)
208245
os.close(fd)
209246

@@ -228,7 +265,7 @@ def process_page(self, page_num: int, pdf_path: str) -> Tuple[int, str]:
228265
overlay=True
229266
)
230267

231-
Path(hocr_output).unlink(missing_ok=True) # comment to keep the hocr file for DEBUG
268+
Path(hocr_output).unlink(missing_ok=True) # DEBUG comment out to keep the hocr files
232269

233270
for _ in range(10):
234271
try:
@@ -244,6 +281,41 @@ def process_page(self, page_num: int, pdf_path: str) -> Tuple[int, str]:
244281

245282
return page_num, temp_pdf_path
246283

284+
def optimize_final_pdf(self, original_pdf_path: Path, ocr_pdf_path: Path) -> None:
285+
"""Post-process the PDF to match original dimensions and apply optimization."""
286+
# print(f"Optimizing OCR'd PDF: {ocr_pdf_path}")
287+
288+
with fitz.open(original_pdf_path) as original_doc:
289+
orig_pages = []
290+
for page in original_doc:
291+
orig_pages.append({
292+
'width': page.rect.width,
293+
'height': page.rect.height,
294+
'mediabox': page.mediabox,
295+
'cropbox': page.cropbox if hasattr(page, 'cropbox') else None
296+
})
297+
298+
temp_path = str(ocr_pdf_path) + ".optimized"
299+
300+
with fitz.open(ocr_pdf_path) as ocr_doc:
301+
for i, page in enumerate(ocr_doc):
302+
if i < len(orig_pages):
303+
orig = orig_pages[i]
304+
page.set_mediabox(orig['mediabox'])
305+
if orig['cropbox']:
306+
page.set_cropbox(orig['cropbox'])
307+
308+
ocr_doc.save(
309+
temp_path,
310+
garbage=4,
311+
deflate=True,
312+
clean=True,
313+
linear=True
314+
)
315+
316+
os.replace(temp_path, ocr_pdf_path)
317+
# print(f"PDF optimization complete. Check final file size.")
318+
247319
def cleanup_temp_pdfs(self):
248320
if self.temp_dir is None:
249321
return

0 commit comments

Comments
 (0)