diff --git a/requirements/test.txt b/requirements/test.txt index c2e93bde..90919da7 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,7 +1,7 @@ pytest == 4.3.0 -pytest-helpers-namespace +pytest-helpers-namespace >= 2019.1.8 pytest-xdist -pytest-cov +pytest-cov >= 2.6.1 python-xmp-toolkit # requires apt-get install libexempi3 # or brew install exempi PyPDF2 >= 1.26.0 diff --git a/setup.py b/setup.py index 83f56540..560434f8 100644 --- a/setup.py +++ b/setup.py @@ -32,156 +32,19 @@ import re # pylint: disable=w0613 -missing_program = ''' -The program '{program}' could not be executed or was not found on your -system PATH. -''' - -unknown_version = ''' -OCRmyPDF requires '{program}' {need_version} or higher. Your system has -'{program}' but we cannot tell what version is installed. Contact the -package maintainer. -''' - -old_version = ''' -OCRmyPDF requires '{program}' {need_version} or higher. Your system appears -to have {found_version}. Please update this program. -''' - -okay_its_optional = ''' -This program is OPTIONAL, so installation of OCRmyPDF can proceed, but -some functionality may be missing. -''' - -not_okay_its_required = ''' -This program is REQUIRED for OCRmyPDF to work. Installation will abort. -''' - -osx_install_advice = ''' -If you have homebrew installed, try these command to install the missing -packages: - brew update - brew upgrade - brew install {package} -''' - -linux_install_advice = ''' -On systems with the aptitude package manager (Debian, Ubuntu), try these -commands: - sudo apt-get update - sudo apt-get install {package} - -On RPM-based systems (Red Hat, Fedora), search for instructions on -installing the RPM for {program}. -''' - - -def get_platform(): - if sys.platform.startswith('freebsd'): - return 'freebsd' - elif sys.platform.startswith('linux'): - return 'linux' - return sys.platform - - -def _error_trailer(program, package, optional, **kwargs): - if optional: - print(okay_its_optional.format(**locals()), file=sys.stderr) - else: - print(not_okay_its_required.format(**locals()), file=sys.stderr) - - if isinstance(package, Mapping): - package = package[get_platform()] - - if get_platform() == 'darwin': - print(osx_install_advice.format(**locals()), file=sys.stderr) - elif get_platform() == 'linux': - print(linux_install_advice.format(**locals()), file=sys.stderr) - - -def error_missing_program(program, package, optional): - print(missing_program.format(**locals()), file=sys.stderr) - _error_trailer(**locals()) - - -def error_unknown_version(program, package, optional, need_version): - print(unknown_version.format(**locals()), file=sys.stderr) - _error_trailer(**locals()) - - -def error_old_version(program, package, optional, need_version, found_version): - print(old_version.format(**locals()), file=sys.stderr) - _error_trailer(**locals()) - - -def check_external_program( - program, - need_version, - package, - version_check_args=None, - version_scrape_regex=re.compile(r'(\d+\.\d+(?:\.\d+)?)'), - optional=False, -): - if not version_check_args: - version_check_args = ['--version'] - print(f'Checking for {program} >= {need_version}...') - try: - result = check_output( - [program] + version_check_args, universal_newlines=True, stderr=STDOUT - ) - except (CalledProcessError, FileNotFoundError): - error_missing_program(program, package, optional) - if not optional: - sys.exit(1) - print(f'Continuing install without {program}') - return - - try: - found_version = version_scrape_regex.search(result).group(1) - except AttributeError: - error_unknown_version(program, package, optional, need_version) - sys.exit(1) - - if found_version < need_version: - error_old_version(program, package, optional, need_version, found_version) - - print(f'Found {program} {found_version}') - command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '') -forced = '--force' in sys.argv +if command.startswith('install') or command in [ + 'check', + 'test', + 'nosetests', + 'easy_install', +]: + forced = '--force' in sys.argv + if forced: + print("The argument --force is deprecated. Please discontinue use.") -if ( - not forced - and command.startswith('install') - or command in ['check', 'test', 'nosetests', 'easy_install'] -): - check_external_program( - program='tesseract', - need_version='4.0.0', # using backport for Travis CI - package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}, - ) - check_external_program( - program='gs', - need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports - package='ghostscript', - ) - check_external_program( - program='unpaper', - need_version='6.1', # latest sane version - package='unpaper', - optional=True, - ) - check_external_program( - program='qpdf', - need_version='8.0.2', # test suite known to fail on 5.1.1 - package='qpdf', - ) - check_external_program( - program='pngquant', need_version='2.0.0', package='pngquant', optional=True - ) - if 'upload' in sys.argv[1:]: print('Use twine to upload the package - setup.py upload is insecure') sys.exit(1) diff --git a/src/ocrmypdf/__main__.py b/src/ocrmypdf/__main__.py index bfd3f6a9..20cf758b 100755 --- a/src/ocrmypdf/__main__.py +++ b/src/ocrmypdf/__main__.py @@ -44,7 +44,14 @@ from .exceptions import ( MissingDependencyError, OutputFileAccessError, ) -from .exec import ghostscript, qpdf, tesseract +from .exec import ( + ghostscript, + qpdf, + tesseract, + check_external_program, + unpaper, + pngquant, +) from .helpers import available_cpu_count, is_file_writable, re_symlink from .pdfa import file_claims_pdfa @@ -67,13 +74,6 @@ if 'IDE_PROJECT_ROOTS' in os.environ: verify_python3_env() -if not tesseract.v4: - complain( - f"Please install tesseract 4.0.0 or newer " - f"(currently installed version is {tesseract.version()})" - ) - sys.exit(ExitCode.missing_dependency) - # ------------- # Parser @@ -633,9 +633,7 @@ def check_options_preprocessing(options, log): if options.clean_final: options.clean = True if options.unpaper_args and not options.clean: - raise argparse.ArgumentError( - None, "--clean is required for --unpaper-args" - ) + raise argparse.ArgumentError(None, "--clean is required for --unpaper-args") if any((options.clean, options.clean_final)): from .exec import unpaper @@ -930,9 +928,6 @@ def log_page_orientations(pdfinfo, _log): def preamble(_log): _log.debug('ocrmypdf ' + VERSION) - _log.debug('tesseract ' + tesseract.version()) - _log.debug('qpdf ' + qpdf.version()) - _log.debug('gs ' + ghostscript.version()) def check_environ(options, _log): @@ -1032,6 +1027,54 @@ def report_output_file_size(options, _log, input_file, output_file): ) +def check_dependency_versions(log): + check_external_program( + log=log, + program='tesseract', + package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}, + version_checker=tesseract.version, + need_version='4.0.0', # using backport for Travis CI + ) + check_external_program( + log=log, + program='gs', + package='ghostscript', + version_checker=ghostscript.version, + need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports + ) + if ghostscript.version() == '9.24': + complain( + "Ghostscript 9.24 contains serious regressions and is not " + "supported. Please upgrade to Ghostscript 9.25 or use an older " + "version." + ) + return ExitCode.missing_dependency + check_external_program( + log=log, + program='unpaper', + package='unpaper', + version_checker=unpaper.version, + need_version='6.1', # latest sane version + optional=True, + ) + if os.environ.get('TRAVIS') != 'true': # Suppress for Ubuntu trusty + check_external_program( + log=log, + program='qpdf', + package='qpdf', + version_checker=qpdf.version, + need_version='8.0.2', + ) + check_external_program( + log=log, + program='pngquant', + package='pngquant', + version_checker=pngquant.version, + need_version='2.0.0', + optional=True, + ) + + def run_pipeline(args=None): options = parser.parse_args(args=args) options.verbose_abbreviated_path = 1 @@ -1048,24 +1091,7 @@ def run_pipeline(args=None): ) preamble(_log) check_options(options, _log) - - # Complain about qpdf version < 7.0.0 - # Suppress the warning if in the test suite, since there are no PPAs - # for qpdf 7.0.0 for Ubuntu trusty (i.e. Travis) - if qpdf.version() < '7.0.0' and not os.environ.get('PYTEST_CURRENT_TEST'): - complain( - f"You are using qpdf version {qpdf.version()} which has known issues including " - f"security vulnerabilities with certain malformed PDFs. Consider " - f"upgrading to version 7.0.0 or newer." - ) - - if ghostscript.version() == '9.24': - complain( - "Ghostscript 9.24 contains serious regressions and is not " - "supported. Please upgrade to Ghostscript 9.25 or use an older " - "version." - ) - return ExitCode.missing_dependency + check_dependency_versions(_log) # Any changes to options will not take effect for options that are already # bound to function parameters in the pipeline. (For example diff --git a/src/ocrmypdf/exec/__init__.py b/src/ocrmypdf/exec/__init__.py index 294d740a..df66b505 100644 --- a/src/ocrmypdf/exec/__init__.py +++ b/src/ocrmypdf/exec/__init__.py @@ -22,6 +22,7 @@ import re import sys from subprocess import run, STDOUT, PIPE, CalledProcessError from ..exceptions import MissingDependencyError +from collections.abc import Mapping def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)'): @@ -58,3 +59,102 @@ def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)'): ) return version + + +missing_program = ''' +The program '{program}' could not be executed or was not found on your +system PATH. +''' + +unknown_version = ''' +OCRmyPDF requires '{program}' {need_version} or higher. Your system has +'{program}' but we cannot tell what version is installed. Contact the +package maintainer. +''' + +old_version = ''' +OCRmyPDF requires '{program}' {need_version} or higher. Your system appears +to have {found_version}. Please update this program. +''' + +okay_its_optional = ''' +This program is OPTIONAL, so installation of OCRmyPDF can proceed, but +some functionality may be missing. +''' + +not_okay_its_required = ''' +This program is REQUIRED for OCRmyPDF to work. Installation will abort. +''' + +osx_install_advice = ''' +If you have homebrew installed, try these command to install the missing +packages: + brew update + brew upgrade + brew install {package} +''' + +linux_install_advice = ''' +On systems with the aptitude package manager (Debian, Ubuntu), try these +commands: + sudo apt-get update + sudo apt-get install {package} + +On RPM-based systems (Red Hat, Fedora), search for instructions on +installing the RPM for {program}. +''' + + +def get_platform(): + if sys.platform.startswith('freebsd'): + return 'freebsd' + elif sys.platform.startswith('linux'): + return 'linux' + return sys.platform + + +def _error_trailer(log, program, package, optional, **kwargs): + if optional: + log.error(okay_its_optional.format(**locals()), file=sys.stderr) + else: + log.error(not_okay_its_required.format(**locals()), file=sys.stderr) + + if isinstance(package, Mapping): + package = package[get_platform()] + + if get_platform() == 'darwin': + log.error(osx_install_advice.format(**locals()), file=sys.stderr) + elif get_platform() == 'linux': + log.error(linux_install_advice.format(**locals()), file=sys.stderr) + + +def error_missing_program(log, program, package, optional): + log.error(missing_program.format(**locals()), file=sys.stderr) + _error_trailer(log, **locals()) + + +def error_unknown_version(log, program, package, optional, need_version): + log.error(unknown_version.format(**locals()), file=sys.stderr) + _error_trailer(log, **locals()) + + +def error_old_version(log, program, package, optional, need_version, found_version): + log.error(old_version.format(**locals()), file=sys.stderr) + _error_trailer(log, **locals()) + + +def check_external_program( + log, program, package, version_checker, need_version, optional=False +): + try: + found_version = version_checker() + except (CalledProcessError, FileNotFoundError, MissingDependencyError): + error_missing_program(log, program, package, optional) + if not optional: + sys.exit(1) + return + + if found_version < need_version: + error_old_version(log, program, package, optional, need_version, found_version) + + log.debug(f'Found {program} {found_version}')