33import json
44import fitz # PyMuPDF
55import pytesseract
6- from PIL import Image , ImageFile
6+ from PIL import Image , ImageFile , ImageOps , ImageFilter
77import io
88
99# Prevent truncated-image crashes in Pillow
1010ImageFile .LOAD_TRUNCATED_IMAGES = True
1111
1212
13+ TESSERACT_CONFIG = "--oem 3 --psm 6 -l eng"
14+
15+
16+ def _preprocess_image (img : Image .Image ) -> Image .Image :
17+ # Normalize orientation, boost contrast, and improve OCR readability.
18+ try :
19+ img = ImageOps .exif_transpose (img )
20+ except Exception :
21+ pass
22+
23+ img = img .convert ("L" )
24+ img = ImageOps .autocontrast (img )
25+
26+ # Scale up small images for better OCR, cap large images to avoid huge memory use.
27+ max_side = max (img .size )
28+ if max_side < 1000 :
29+ scale = 1000 / max_side
30+ img = img .resize ((int (img .size [0 ] * scale ), int (img .size [1 ] * scale )), Image .BICUBIC )
31+ elif max_side > 3000 :
32+ scale = 3000 / max_side
33+ img = img .resize ((int (img .size [0 ] * scale ), int (img .size [1 ] * scale )), Image .BICUBIC )
34+
35+ img = img .filter (ImageFilter .SHARPEN )
36+ return img
37+
38+
39+ def _ocr_image (img : Image .Image ) -> str :
40+ img = _preprocess_image (img )
41+ text = pytesseract .image_to_string (img , config = TESSERACT_CONFIG )
42+ return text or ""
43+
44+
45+ def _render_page_to_image (page ) -> Image .Image :
46+ # Render PDF page to raster image for OCR fallback.
47+ pix = page .get_pixmap (dpi = 200 )
48+ mode = "RGB" if pix .alpha == 0 else "RGBA"
49+ return Image .frombytes (mode , [pix .width , pix .height ], pix .samples )
50+
51+
1352def process_pdf (buffer : bytes ) -> str :
1453 try :
1554 pdf = fitz .open (stream = buffer , filetype = "pdf" )
1655 text = ""
1756 for page in pdf :
1857 text += page .get_text () or ""
58+ if text .strip ():
59+ return text
60+
61+ # Fallback: render pages and OCR if no embedded text exists.
62+ for page in pdf :
63+ img = _render_page_to_image (page )
64+ text += _ocr_image (img )
1965 return text
2066 except Exception as e :
2167 # Output to stderr so Node can capture useful debugging info
@@ -26,8 +72,7 @@ def process_pdf(buffer: bytes) -> str:
2672def process_image (buffer : bytes ) -> str :
2773 try :
2874 img = Image .open (io .BytesIO (buffer ))
29- text = pytesseract .image_to_string (img )
30- return text or ""
75+ return _ocr_image (img )
3176 except Exception as e :
3277 print (f"Image processing error: { e } " , file = sys .stderr )
3378 return ""
@@ -49,17 +94,22 @@ def main():
4994 # Detect PDFs safely
5095 is_pdf = buffer .startswith (b"%PDF" ) or buffer [:4 ] == b"\x25 \x50 \x44 \x46 "
5196
52- if is_pdf :
53- text = process_pdf (buffer )
54- else :
55- text = process_image (buffer )
97+ error = ""
98+ try :
99+ if is_pdf :
100+ text = process_pdf (buffer )
101+ else :
102+ text = process_image (buffer )
103+ except Exception as e :
104+ error = str (e )
105+ text = ""
56106
57107 # Always output valid JSON
58108 try :
59- print (json .dumps ({"text" : text }))
109+ print (json .dumps ({"text" : text , "error" : error }))
60110 except Exception as e :
61111 print (f"JSON output error: { e } " , file = sys .stderr )
62- print ('{"text": ""}' )
112+ print ('{"text": "", "error": "json_output_error" }' )
63113
64114
65115if __name__ == "__main__" :
0 commit comments