"""Tesseract OCR helper. Pure-Python wrapper that uses pytesseract. Tesseract binary must be installed separately (Windows installer from UB-Mannheim). The captcha solver falls back to this if Florence-2 is unavailable. Install: pip install pytesseract Windows: https://github.com/UB-Mannheim/tesseract/wiki (use the .exe installer) Linux: apt install tesseract-ocr macOS: brew install tesseract """ from __future__ import annotations import os import shutil from typing import Optional import pytesseract from PIL import Image def is_available() -> bool: """Return True if tesseract binary is on PATH, in TESSERACT_CMD env, or in standard locations.""" if shutil.which("tesseract") or shutil.which("tesseract.exe"): return True if os.environ.get("TESSERACT_CMD") and os.path.exists(os.environ["TESSERACT_CMD"]): return True for p in ( r"C:\Program Files\Tesseract-OCR\tesseract.exe", r"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe", "/usr/bin/tesseract", "/usr/local/bin/tesseract", ): if os.path.exists(p): pytesseract.pytesseract.tesseract_cmd = p return True return False def _preprocess_for_ocr(pil_image: Image.Image) -> Image.Image: """Upscale + add padding to help tesseract read cramped captcha images. Tesseract needs significant whitespace between characters and at borders. Small captchas (220x70) often bleed characters together, producing OCR errors like "3+5=?" -> "34+5=?". Scaling 2-3x and adding 20px padding fixes most of these. """ w, h = pil_image.size # Upscale if smaller than 400px wide if w < 400: scale = 3 if w < 300 else 2 new_size = (w * scale, h * scale) pil_image = pil_image.resize(new_size, Image.LANCZOS) # Add 20px white border all around if pil_image.mode != "RGB": pil_image = pil_image.convert("RGB") bordered = Image.new("RGB", (pil_image.size[0] + 40, pil_image.size[1] + 40), (255, 255, 255)) bordered.paste(pil_image, (20, 20)) return bordered def ocr_image( pil_image: Image.Image, psm: int = 7, lang: str = "eng", whitelist: Optional[str] = None, ) -> str: """Run Tesseract on a PIL image. psm=7: single line psm=8: single word psm=6: assume uniform block """ config_parts = [f"--psm {psm}", f"-l {lang}"] if whitelist: config_parts.append(f"-c tessedit_char_whitelist={whitelist}") return pytesseract.image_to_string(pil_image, config=" ".join(config_parts)).strip() def ocr_captcha_math(pil_image: Image.Image) -> str: """OCR a math captcha. Upscales, then tries psm modes in priority order. Returns the first non-empty result. PSM 7 (single line) is the most reliable for math captchas after upscaling; falls back to others if empty. """ pil_image = _preprocess_for_ocr(pil_image) for psm in (7, 6, 13, 8, 11): try: txt = ocr_image(pil_image, psm=psm, whitelist="0123456789+-*/=()xX? ").strip() if txt: return txt except Exception: continue return "" def ocr_captcha_text(pil_image: Image.Image) -> str: """OCR a text-only captcha (alphanumeric).""" pil_image = _preprocess_for_ocr(pil_image) return ocr_image(pil_image, psm=7, whitelist="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")