Spaces:
Sleeping
Sleeping
File size: 6,010 Bytes
abcd0c2 c5638b0 abcd0c2 c5638b0 abcd0c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | """
OCR utilities for the MarkItDown API.
Two engines are provided:
ocr_image(source)
RapidOCR singleton for raster images (JPEG, PNG, WEBP, etc.).
Accepts bytes, a local file path string, an HTTP/HTTPS URL string,
a numpy.ndarray, or a PIL.Image instance.
ocr_pdf(source, dpi=150)
Scanned-PDF fallback. Renders each page with pypdfium2, then feeds
the rendered PIL image through ocr_image. Returns all pages joined
with double newlines.
Both functions return a plain string and never raise; errors are logged and
an empty string is returned on failure.
"""
from __future__ import annotations
import io
import threading
from typing import Union
from urllib.parse import urlparse
import numpy as np
from logger import get_logger
logger = get_logger(__name__)
# ---------------------------------------------------------------------------
# RapidOCR singleton
# ---------------------------------------------------------------------------
_lock = threading.Lock()
_engine = None
def _get_engine():
"""Return the shared RapidOCR instance, initialising it on first call."""
global _engine
if _engine is None:
with _lock:
if _engine is None:
from rapidocr_onnxruntime import RapidOCR
_engine = RapidOCR(
Det={"use_cuda": False, "use_dml": False},
Cls={"use_cuda": False, "use_dml": False},
Rec={"use_cuda": False, "use_dml": False},
print_verbose=False,
)
logger.info("RapidOCR engine initialised")
return _engine
# ---------------------------------------------------------------------------
# Input normalisation
# ---------------------------------------------------------------------------
def _to_numpy(source) -> Union[np.ndarray, str]:
"""Normalise *source* to a numpy array or a local file path string.
Accepted input types:
PIL.Image — converted directly to ndarray.
bytes — decoded via PIL then converted to ndarray.
str — HTTP/HTTPS URL fetched then decoded; local paths returned as-is.
np.ndarray — returned unchanged.
"""
from PIL import Image
def _pil_to_array(img: Image.Image) -> np.ndarray:
if img.mode not in ("RGB", "L", "RGBA"):
img = img.convert("RGB")
return np.array(img)
if isinstance(source, np.ndarray):
return source
if isinstance(source, Image.Image):
return _pil_to_array(source)
if isinstance(source, (bytes, bytearray, memoryview)):
return _pil_to_array(Image.open(io.BytesIO(bytes(source))))
if isinstance(source, str):
parsed = urlparse(source)
if parsed.scheme in {"http", "https"}:
import httpx
resp = httpx.get(source, follow_redirects=True, timeout=30)
resp.raise_for_status()
return _pil_to_array(Image.open(io.BytesIO(resp.content)))
# Local file path — RapidOCR accepts it directly.
return source
raise TypeError(
f"ocr_image expects bytes, str (URL or path), numpy.ndarray, or PIL.Image; "
f"received {type(source).__name__!r}"
)
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def ocr_image(
source,
*,
use_det: bool = True,
use_cls: bool = True,
use_rec: bool = True,
text_score: float = 0.5,
) -> str:
"""Extract text from an image using RapidOCR.
Parameters
----------
source:
Input image — bytes, URL string, local path string, numpy.ndarray,
or PIL.Image.
use_det, use_cls, use_rec:
RapidOCR pipeline stages (detection, classification, recognition).
text_score:
Minimum confidence threshold for accepted text lines.
Returns
-------
str
Recognised text lines joined by newlines, or an empty string when
no text is detected.
"""
engine = _get_engine()
img = _to_numpy(source)
result, _ = engine(
img,
use_det=use_det,
use_cls=use_cls,
use_rec=use_rec,
text_score=text_score,
)
if not result:
return ""
return "\n".join(item[1] for item in result if len(item) > 1 and item[1])
def ocr_pdf(source: Union[str, bytes], *, dpi: int = 150) -> str:
"""Extract text from a scanned (image-only) PDF using pypdfium2 and RapidOCR.
Each page is rendered to a PIL image in memory (no temporary files are
written), then passed through ocr_image. All page outputs are joined
with double newlines.
Parameters
----------
source:
Local file path (str) or raw PDF bytes.
dpi:
Rendering resolution. 150 balances speed and OCR quality for most
document types. Increase to 200-300 for small or dense text.
Returns
-------
str
Concatenated OCR text from all pages, or an empty string on failure.
"""
try:
import pypdfium2 as pdfium
except ImportError:
logger.error("ocr_pdf | pypdfium2 not installed; run: pip install pypdfium2")
return ""
try:
pdf = pdfium.PdfDocument(source)
scale = dpi / 72.0 # pypdfium2 native resolution is 72 dpi
page_texts: list[str] = []
for page_index in range(len(pdf)):
page = pdf[page_index]
bitmap = page.render(scale=scale, rotation=0)
pil_image = bitmap.to_pil()
logger.debug("ocr_pdf | processing page %d/%d", page_index + 1, len(pdf))
page_text = ocr_image(pil_image)
if page_text:
page_texts.append(page_text)
pdf.close()
return "\n\n".join(page_texts)
except Exception as exc:
logger.error("ocr_pdf | failed | error=%s", exc, exc_info=True)
return ""
|