diff --git a/python/pyproject.toml b/python/pyproject.toml index 2b1f055..1c28c13 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -42,6 +42,9 @@ Changelog = "https://github.com/RapidAI/RapidOCR/releases" [project.scripts] rapidocr = "rapidocr.main:main" +[project.optional-dependencies] +rtl = ["python-bidi>=0.4.2,<1.0"] + [tool.setuptools] include-package-data = true platforms = ["Any"] diff --git a/python/rapidocr/ch_ppocr_rec/main.py b/python/rapidocr/ch_ppocr_rec/main.py index e902717..b22f81a 100644 --- a/python/rapidocr/ch_ppocr_rec/main.py +++ b/python/rapidocr/ch_ppocr_rec/main.py @@ -24,7 +24,8 @@ from rapidocr.inference_engine.base import FileInfo, get_engine from ..utils.download_file import DownloadFile, DownloadFileInput from ..utils.log import logger from ..utils.model_resolver import normalize_lang -from ..utils.utils import reorder_bidi_for_display +from ..utils.typings import LangRec +from ..utils.utils import reorder_bidi_for_display, validate_rtl_dependency from ..utils.vis_res import VisRes from .typings import TextRecInput, TextRecOutput from .utils import CTCLabelDecode @@ -32,10 +33,14 @@ from .utils import CTCLabelDecode DEFAULT_DICT_PATH = Path(__file__).parent.parent / "models" / "ppocr_keys_v1.txt" DEFAULT_DICT_URL = "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v2.0.7/paddle/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer/ppocr_keys_v1.txt" DEFAULT_MODEL_PATH = Path(__file__).parent.parent / "models" +RTL_LANGS = {LangRec.ARABIC.value} class TextRecognizer: def __init__(self, cfg: Dict[str, Any]): + if normalize_lang(cfg.lang_type) in RTL_LANGS: + validate_rtl_dependency() + self.session = get_engine(cfg.engine_type)(cfg) # onnx has inner character, other engine get or download character_dict_path @@ -50,8 +55,6 @@ class TextRecognizer: self.cfg = cfg - self.RTL_LANGS = {"arabic", "fa", "ur"} - def get_character_dict(self, cfg): character = None dict_path = cfg.get("rec_keys_path", None) @@ -139,7 +142,7 @@ class TextRecognizer: all_line_results, all_word_results = list(zip(*rec_res)) txts, scores = list(zip(*all_line_results)) - if normalize_lang(self.cfg.lang_type) in self.RTL_LANGS: + if normalize_lang(self.cfg.lang_type) in RTL_LANGS: txts = reorder_bidi_for_display(txts) elapse = time.perf_counter() - start_time diff --git a/python/rapidocr/utils/utils.py b/python/rapidocr/utils/utils.py index 583a453..0591914 100644 --- a/python/rapidocr/utils/utils.py +++ b/python/rapidocr/utils/utils.py @@ -12,16 +12,22 @@ import cv2 import numpy as np +def validate_rtl_dependency(): + try: + from bidi.algorithm import get_display # noqa: F401 + except ImportError as exc: + raise ModuleNotFoundError( + "RTL recognition requires the optional dependency 'python-bidi'. " + "Install it with: pip install 'rapidocr[rtl]'" + ) from exc + + def reorder_bidi_for_display( texts: Tuple[Union[str, bytes], ...], ) -> Tuple[Union[str, bytes], ...]: - try: - from bidi.algorithm import get_display - except ImportError as e: - raise ModuleNotFoundError( - "Required dependency 'python-bidi' is not installed. " - "Install it with: pip install python-bidi" - ) from e + validate_rtl_dependency() + + from bidi.algorithm import get_display # noqa: F401 return tuple([get_display(text) for text in texts]) diff --git a/python/tests/test_rtl_dependency.py b/python/tests/test_rtl_dependency.py new file mode 100644 index 0000000..48c6c6e --- /dev/null +++ b/python/tests/test_rtl_dependency.py @@ -0,0 +1,70 @@ +# -*- encoding: utf-8 -*- +# @Author: SWHL +# @Contact: liekkaskono@163.com +import builtins +import sys +import types +from pathlib import Path +from types import SimpleNamespace + +import pytest + +root_dir = Path(__file__).resolve().parent.parent +sys.path.append(str(root_dir)) + +from rapidocr.ch_ppocr_rec import main as rec_main +from rapidocr.utils import utils +from rapidocr.utils.model_resolver import normalize_lang +from rapidocr.utils.typings import EngineType, LangRec + + +def test_validate_rtl_dependency_reports_extra(monkeypatch): + original_import = builtins.__import__ + + def missing_bidi(name, *args, **kwargs): + if name.startswith("bidi"): + raise ModuleNotFoundError("No module named 'bidi'") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", missing_bidi) + + with pytest.raises(ModuleNotFoundError, match=r"rapidocr\[rtl\]"): + utils.validate_rtl_dependency() + + +def test_reorder_bidi_for_display_uses_bidi_algorithm(monkeypatch): + bidi_module = types.ModuleType("bidi") + algorithm_module = types.ModuleType("bidi.algorithm") + algorithm_module.get_display = lambda text: text[::-1] + bidi_module.algorithm = algorithm_module + monkeypatch.setitem(sys.modules, "bidi", bidi_module) + monkeypatch.setitem(sys.modules, "bidi.algorithm", algorithm_module) + + assert utils.reorder_bidi_for_display(("abc", "123")) == ("cba", "321") + + +def test_arabic_dependency_is_checked_before_engine_initialization(monkeypatch): + engine_called = False + + def fail_get_engine(_engine_type): + nonlocal engine_called + engine_called = True + raise AssertionError( + "engine initialization must happen after dependency validation" + ) + + def missing_dependency(): + raise ModuleNotFoundError("RTL dependency is missing") + + monkeypatch.setattr(rec_main, "get_engine", fail_get_engine) + monkeypatch.setattr(rec_main, "validate_rtl_dependency", missing_dependency) + + cfg = SimpleNamespace(lang_type=LangRec.ARABIC, engine_type=EngineType.ONNXRUNTIME) + with pytest.raises(ModuleNotFoundError, match="RTL dependency is missing"): + rec_main.TextRecognizer(cfg) + + assert engine_called is False + + +def test_arabic_enum_is_normalized_for_rtl_detection(): + assert normalize_lang(LangRec.ARABIC) in rec_main.RTL_LANGS