diff --git a/src/ocrmypdf/_exec/tesseract.py b/src/ocrmypdf/_exec/tesseract.py index 7f7c06a4..5dc5024b 100644 --- a/src/ocrmypdf/_exec/tesseract.py +++ b/src/ocrmypdf/_exec/tesseract.py @@ -9,8 +9,10 @@ import logging import os +import re import shutil from collections import namedtuple +from distutils.version import StrictVersion from os import fspath from pathlib import Path from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired @@ -53,31 +55,29 @@ class TesseractLoggerAdapter(logging.LoggerAdapter): return '[tesseract] %s' % (msg), kwargs +class TesseractVersion(StrictVersion): + version_re = re.compile( + r''' + ^(\d+) \. (\d+) (\. (\d+))? # groups: 1/major, 2/minor, 3/[skip], 4/patch + [-]? # optional hyphen separator + (?:(alpha|beta|rc|dev)[.\-\ ]?(\d+)?)? # 5/prerelease, 6/prerelease_num + $ + ''', + re.VERBOSE | re.ASCII, + ) + + def parse(self, vstring): + try: + super().parse(vstring) + except TypeError as e: + if 'int() argument must be a string' in str(e): + super().parse(vstring + '0') + + def version(): return get_version('tesseract', regex=r'tesseract\s(.+)') -def has_textonly_pdf(langs=None): - """Does Tesseract have textonly_pdf capability? - - Available in v4.00.00alpha since January 2017. Best to - parse the parameter list. - """ - args_tess = tess_base_args(langs, engine_mode=None) + ['--print-parameters', 'pdf'] - params = '' - try: - proc = run(args_tess, check=True, stdout=PIPE, stderr=STDOUT) - params = proc.stdout - except CalledProcessError as e: - raise MissingDependencyError( - "Could not --print-parameters from tesseract. This can happen if the " - "TESSDATA_PREFIX environment is not set to a valid tessdata folder. " - ) from e - if b'textonly_pdf' in params: - return True - return False - - def has_user_words(): """Does Tesseract have --user-words capability? diff --git a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py index bbcb6720..bcb5de7f 100644 --- a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py +++ b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py @@ -81,19 +81,13 @@ def check_options(options): package={'linux': 'tesseract-ocr'}, version_checker=tesseract.version, need_version='4.0.0', # using backport for Travis CI + version_parser=tesseract.TesseractVersion, ) # Decide on what renderer to use if options.pdf_renderer == 'auto': options.pdf_renderer = 'sandwich' - if options.pdf_renderer == 'sandwich' and not tesseract.has_textonly_pdf( - set(options.languages) - ): - raise MissingDependencyError( - "You are using an alpha version of Tesseract 4.0 that does not support " - "the textonly_pdf parameter. We don't support versions this old." - ) if not tesseract.has_user_words() and (options.user_words or options.user_patterns): log.warning( "Tesseract 4.0 ignores --user-words and --user-patterns, so these " diff --git a/src/ocrmypdf/subprocess/__init__.py b/src/ocrmypdf/subprocess/__init__.py index 2755337b..99f052c6 100644 --- a/src/ocrmypdf/subprocess/__init__.py +++ b/src/ocrmypdf/subprocess/__init__.py @@ -13,14 +13,17 @@ import re import sys from collections.abc import Mapping from contextlib import suppress -from distutils.version import LooseVersion +from distutils.version import LooseVersion, Version from functools import lru_cache from pathlib import Path from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen from subprocess import run as subprocess_run +from typing import Callable, Optional, Type, Union from ocrmypdf.exceptions import MissingDependencyError +# pylint: disable=logging-format-interpolation + log = logging.getLogger(__name__) @@ -264,13 +267,30 @@ def _error_old_version(program, package, need_version, found_version, required_f def check_external_program( *, - program, - package, - version_checker, - need_version, - required_for=None, + program: str, + package: str, + version_checker: Union[str, Callable], + need_version: str, + required_for: Optional[str] = None, recommended=False, + version_parser: Type[Version] = LooseVersion, ): + """Check for required version of external program and raise exception if not. + + Args: + program: The name of the program to test. + package: The name of a software package that typically supplies this program. + Usually the same as program. + version_check: A callable without arguments that retrieves the installed + version of program. + need_version: The minimum required version. + required_for: The name of an argument of feature that requires this program. + recommended: If this external program is recommended, instead of raising + an exception, log a warning and allow execution to continue. + version_parser: A class that should be used to parse and compare version + numbers. Used when version numbers do not follow standard conventions. + """ + try: if callable(version_checker): found_version = version_checker() @@ -279,7 +299,7 @@ def check_external_program( except (CalledProcessError, FileNotFoundError, MissingDependencyError): _error_missing_program(program, package, required_for, recommended) if not recommended: - raise MissingDependencyError() + raise MissingDependencyError(program) return def remove_leading_v(s): @@ -290,9 +310,9 @@ def check_external_program( found_version = remove_leading_v(found_version) need_version = remove_leading_v(need_version) - if found_version and LooseVersion(found_version) < LooseVersion(need_version): + if found_version and version_parser(found_version) < version_parser(need_version): _error_old_version(program, package, need_version, found_version, required_for) if not recommended: - raise MissingDependencyError() + raise MissingDependencyError(program) log.debug('Found %s %s', program, found_version) diff --git a/tests/test_validation.py b/tests/test_validation.py index 745ea15e..6bec04e3 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -13,6 +13,7 @@ import pytest from ocrmypdf import _validation as vd from ocrmypdf._concurrent import NullProgressBar, SerialExecutor +from ocrmypdf._exec.tesseract import TesseractVersion from ocrmypdf._plugin_manager import get_plugin_manager from ocrmypdf.api import create_options from ocrmypdf.cli import get_parser @@ -52,29 +53,23 @@ def test_hocr_notlatin_warning(caplog): def test_old_ghostscript(caplog): - with patch('ocrmypdf._exec.ghostscript.version', return_value='9.19'), patch( - 'ocrmypdf._exec.tesseract.has_textonly_pdf', return_value=True - ): + with patch('ocrmypdf._exec.ghostscript.version', return_value='9.19'): vd._check_options( *make_opts_pm(language='chi_sim', output_type='pdfa'), {'chi_sim'} ) assert 'does not work correctly' in caplog.text - with patch('ocrmypdf._exec.ghostscript.version', return_value='9.18'), patch( - 'ocrmypdf._exec.tesseract.has_textonly_pdf', return_value=True - ): + with patch('ocrmypdf._exec.ghostscript.version', return_value='9.18'): with pytest.raises(MissingDependencyError): vd._check_options(*make_opts_pm(output_type='pdfa-3'), set()) - with patch('ocrmypdf._exec.ghostscript.version', return_value='9.24'), patch( - 'ocrmypdf._exec.tesseract.has_textonly_pdf', return_value=True - ): + with patch('ocrmypdf._exec.ghostscript.version', return_value='9.24'): with pytest.raises(MissingDependencyError): vd._check_options(*make_opts_pm(), set()) def test_old_tesseract_error(): - with patch('ocrmypdf._exec.tesseract.has_textonly_pdf', return_value=False): + with patch('ocrmypdf._exec.tesseract.version', return_value='4.00.00alpha'): with pytest.raises(MissingDependencyError): opts = make_opts(pdf_renderer='sandwich', language='eng') plugin_manager = get_plugin_manager(opts.plugins) @@ -227,17 +222,20 @@ def test_version_comparison(): version_checker=lambda: '10.0', need_version='8.0.2', ) - vd.check_external_program( - program="tesseract", - package="tesseract", - version_checker=lambda: '4.0.0-beta.1', - need_version='4.0.0', - ) + with pytest.raises(MissingDependencyError): + vd.check_external_program( + program="tesseract", + package="tesseract", + version_checker=lambda: '4.0.0-beta.1', + need_version='4.0.0', + version_parser=TesseractVersion, + ) vd.check_external_program( program="tesseract", package="tesseract", version_checker=lambda: 'v5.0.0-alpha.20200201', need_version='4.0.0', + version_parser=TesseractVersion, ) with pytest.raises(MissingDependencyError): vd.check_external_program( @@ -277,11 +275,9 @@ def test_pagesegmode_warning(caplog): def test_two_languages(): - with patch('ocrmypdf._exec.tesseract.has_textonly_pdf', return_value=True) as mock: - vd._check_options( - *make_opts_pm(language='fakelang1+fakelang2'), {'fakelang1', 'fakelang2'} - ) - mock.assert_called() + vd._check_options( + *make_opts_pm(language='fakelang1+fakelang2'), {'fakelang1', 'fakelang2'} + ) def test_sidecar_equals_output(resources, no_outpdf):