diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 0806880e..6e67d808 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -321,9 +321,9 @@ def rasterize_preview(input_file, page_context): output_file = page_context.get_path('rasterize_preview.jpg') canvas_dpi = get_canvas_square_dpi(page_context.pageinfo, page_context.options) page_dpi = get_page_square_dpi(page_context.pageinfo, page_context.options) - ghostscript.rasterize_pdf( - input_file, - output_file, + page_context.plugin_manager.hook.rasterize_pdf_page( + input_file=input_file, + output_file=output_file, raster_device='jpeggray', raster_dpi=canvas_dpi, page_dpi=page_dpi, @@ -430,9 +430,9 @@ def rasterize( canvas_dpi = get_canvas_square_dpi(pageinfo, page_context.options) page_dpi = get_page_square_dpi(pageinfo, page_context.options) - ghostscript.rasterize_pdf( - input_file, - output_file, + page_context.plugin_manager.hook.rasterize_pdf_page( + input_file=input_file, + output_file=output_file, raster_device=device, raster_dpi=canvas_dpi, page_dpi=page_dpi, diff --git a/src/ocrmypdf/_plugin_manager.py b/src/ocrmypdf/_plugin_manager.py index 1fbc1b76..9f328c69 100644 --- a/src/ocrmypdf/_plugin_manager.py +++ b/src/ocrmypdf/_plugin_manager.py @@ -33,7 +33,10 @@ def get_plugin_manager(plugins: List[str], builtins=True): pm.add_hookspecs(pluginspec) if builtins: - all_plugins = ['ocrmypdf.builtin_plugins'] + plugins + all_plugins = [ + 'ocrmypdf.builtin_plugins.ghostscript', + 'ocrmypdf.builtin_plugins.tesseract_ocr', + ] + plugins else: all_plugins = plugins for name in all_plugins: diff --git a/src/ocrmypdf/_validation.py b/src/ocrmypdf/_validation.py index 941e3522..3a92d34c 100644 --- a/src/ocrmypdf/_validation.py +++ b/src/ocrmypdf/_validation.py @@ -27,7 +27,6 @@ from shutil import copyfileobj import PIL -from ocrmypdf._plugin_manager import get_plugin_manager from ocrmypdf._unicodefun import verify_python3_env from ocrmypdf.exceptions import ( BadArgsError, @@ -35,14 +34,7 @@ from ocrmypdf.exceptions import ( MissingDependencyError, OutputFileAccessError, ) -from ocrmypdf.exec import ( - check_external_program, - ghostscript, - jbig2enc, - pngquant, - tesseract, - unpaper, -) +from ocrmypdf.exec import check_external_program, jbig2enc, pngquant, unpaper from ocrmypdf.helpers import ( is_file_writable, is_iterable_notstr, @@ -92,10 +84,6 @@ def check_options_languages(options, plugin_manager): def check_options_output(options): - # We have these constraints to check for. - # 1. Ghostscript < 9.20 mangles multibyte Unicode - # 2. hocr doesn't work on non-Latin languages (so don't select it) - is_latin = options.languages.issubset(HOCR_OK_LANGS) if options.pdf_renderer == 'hocr' and not is_latin: @@ -106,25 +94,6 @@ def check_options_output(options): ) log.warning(msg) - if ghostscript.version() < '9.20' and options.output_type != 'pdf' and not is_latin: - # https://bugs.ghostscript.com/show_bug.cgi?id=696874 - # Ghostscript < 9.20 fails to encode multibyte characters properly - msg = ( - "The installed version of Ghostscript does not work correctly " - "with the OCR languages you specified. Use --output-type pdf or " - "upgrade to Ghostscript 9.20 or later to avoid this issue." - ) - msg += f"Found Ghostscript {ghostscript.version()}" - log.warning(msg) - - if options.output_type == 'pdfa': - options.output_type = 'pdfa-2' - - if options.output_type == 'pdfa-3' and ghostscript.version() < '9.19': - raise MissingDependencyError( - "--output-type pdfa-3 requires Ghostscript 9.19 or later" - ) - lossless_reconstruction = False if not any( ( @@ -291,7 +260,6 @@ def check_options(options, plugin_manager): check_options_optimizing(options) check_options_advanced(options) check_options_pillow(options) - check_dependency_versions(options) plugin_manager.hook.check_options(options=options) @@ -432,19 +400,3 @@ def report_output_file_size(options, input_file, output_file): f"The output file size is {ratio:.2f}× larger than the input file.\n" f"{explanation}" ) - - -def check_dependency_versions(options): - check_external_program( - program='gs', - package='ghostscript', - version_checker=ghostscript.version, - need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports - ) - gs_version = ghostscript.version() - if gs_version in ('9.24', '9.51'): - raise MissingDependencyError( - f"Ghostscript {gs_version} contains serious regressions and is not " - "supported. Please upgrade to a newer version, or downgrade to the " - "previous version." - ) diff --git a/src/ocrmypdf/builtin_plugins/__init__.py b/src/ocrmypdf/builtin_plugins/__init__.py index e5fd494e..0ed32bc2 100644 --- a/src/ocrmypdf/builtin_plugins/__init__.py +++ b/src/ocrmypdf/builtin_plugins/__init__.py @@ -14,5 +14,3 @@ # # You should have received a copy of the GNU General Public License # along with OCRmyPDF. If not, see . - -from ocrmypdf.builtin_plugins.tesseract_ocr import * diff --git a/src/ocrmypdf/builtin_plugins/ghostscript.py b/src/ocrmypdf/builtin_plugins/ghostscript.py new file mode 100644 index 00000000..92f39008 --- /dev/null +++ b/src/ocrmypdf/builtin_plugins/ghostscript.py @@ -0,0 +1,90 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This file is part of OCRmyPDF. +# +# OCRmyPDF is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# OCRmyPDF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OCRmyPDF. If not, see . + +import logging +from pathlib import Path + +from ocrmypdf import hookimpl +from ocrmypdf._validation import HOCR_OK_LANGS +from ocrmypdf.exceptions import MissingDependencyError +from ocrmypdf.exec import check_external_program, ghostscript +from ocrmypdf.helpers import Resolution + +log = logging.getLogger(__name__) + + +@hookimpl +def check_options(options): + gs_version = ghostscript.version() + check_external_program( + program='gs', + package='ghostscript', + version_checker=gs_version, + need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports + ) + if gs_version in ('9.24', '9.51'): + raise MissingDependencyError( + f"Ghostscript {gs_version} contains serious regressions and is not " + "supported. Please upgrade to a newer version, or downgrade to the " + "previous version." + ) + + # We have these constraints to check for. + # 1. Ghostscript < 9.20 mangles multibyte Unicode + # 2. hocr doesn't work on non-Latin languages (so don't select it) + is_latin = options.languages.issubset(HOCR_OK_LANGS) + if gs_version < '9.20' and options.output_type != 'pdf' and not is_latin: + # https://bugs.ghostscript.com/show_bug.cgi?id=696874 + # Ghostscript < 9.20 fails to encode multibyte characters properly + msg = ( + "The installed version of Ghostscript does not work correctly " + "with the OCR languages you specified. Use --output-type pdf or " + "upgrade to Ghostscript 9.20 or later to avoid this issue." + ) + msg += f"Found Ghostscript {gs_version}" + log.warning(msg) + + if options.output_type == 'pdfa': + options.output_type = 'pdfa-2' + + if options.output_type == 'pdfa-3' and ghostscript.version() < '9.19': + raise MissingDependencyError( + "--output-type pdfa-3 requires Ghostscript 9.19 or later" + ) + + +@hookimpl +def rasterize_pdf_page( + input_file: Path, + output_file: Path, + raster_device: str, + raster_dpi: Resolution, + pageno: int, + page_dpi: Resolution = None, + rotation: int = None, + filter_vector: bool = False, +): + return ghostscript.rasterize_pdf( + input_file, + output_file, + raster_device=raster_device, + raster_dpi=raster_dpi, + pageno=pageno, + page_dpi=page_dpi, + rotation=rotation, + filter_vector=filter_vector, + ) diff --git a/src/ocrmypdf/exec/_support.py b/src/ocrmypdf/exec/_support.py index 60d22a83..5f52ac87 100644 --- a/src/ocrmypdf/exec/_support.py +++ b/src/ocrmypdf/exec/_support.py @@ -280,7 +280,10 @@ def check_external_program( recommended=False, ): try: - found_version = version_checker() + if callable(version_checker): + found_version = version_checker() + else: + found_version = version_checker except (CalledProcessError, FileNotFoundError, MissingDependencyError): _error_missing_program(program, package, required_for, recommended) if not recommended: diff --git a/src/ocrmypdf/exec/ghostscript.py b/src/ocrmypdf/exec/ghostscript.py index d7407726..a43b972b 100644 --- a/src/ocrmypdf/exec/ghostscript.py +++ b/src/ocrmypdf/exec/ghostscript.py @@ -20,7 +20,6 @@ import logging import os import re -import warnings from io import BytesIO from os import fspath from pathlib import Path @@ -92,22 +91,7 @@ def rasterize_pdf( rotation: int = None, filter_vector: bool = False, ): - """Rasterize one page of a PDF at resolution raster_dpi in canvas units. - - The image is sized to match the integer pixels dimensions implied by - raster_dpi even if those numbers are noninteger. The image's DPI will - be overridden with the values in page_dpi. - - :param input_file: pathlike - :param output_file: pathlike - :param raster_device: - :param raster_dpi: resolution at which to rasterize page - :param pageno: page number to rasterize (beginning at page 1) - :param page_dpi: resolution tuple (x, y) overriding output image DPI - :param rotation: 0, 90, 180, 270: clockwise angle to rotate page - :param filter_vector: if True, remove vector graphics objects - :return: - """ + """Rasterize one page of a PDF at resolution raster_dpi in canvas units.""" raster_dpi = raster_dpi.round(6) if not page_dpi: page_dpi = raster_dpi diff --git a/src/ocrmypdf/pluginspec.py b/src/ocrmypdf/pluginspec.py index dd659458..fd2498b2 100644 --- a/src/ocrmypdf/pluginspec.py +++ b/src/ocrmypdf/pluginspec.py @@ -24,6 +24,8 @@ from typing import AbstractSet, Optional import pluggy from PIL import Image +from ocrmypdf.helpers import Resolution + hookspec = pluggy.HookspecMarker('ocrmypdf') # pylint: disable=unused-argument @@ -66,6 +68,35 @@ def validate(pdfinfo: 'PdfInfo', options: Namespace) -> None: """ +@hookspec(firstresult=True) +def rasterize_pdf_page( + input_file: Path, + output_file: Path, + raster_device: str, + raster_dpi: Resolution, + pageno: int, + page_dpi: Resolution = None, + rotation: int = None, + filter_vector: bool = False, +) -> None: + """Rasterize one page of a PDF at resolution raster_dpi in canvas units. + + The image is sized to match the integer pixels dimensions implied by + raster_dpi even if those numbers are noninteger. The image's DPI will + be overridden with the values in page_dpi. + + Args: + raster_device: type of image to produce at output_file + raster_dpi: resolution at which to rasterize page + pageno: page number to rasterize (beginning at page 1) + page_dpi: resolution, overriding output image DPI + rotation: cardinal angle, clockwise, to rotate page + filter_vector: if True, remove vector graphics objects + Returns: + None + """ + + @hookspec(firstresult=True) def filter_ocr_image(page: 'PageContext', image: Image) -> Image: """Called to filter the image before it is sent to OCR. diff --git a/tests/test_main.py b/tests/test_main.py index 6f9d7b50..0b913a14 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -39,7 +39,6 @@ from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo check_ocrmypdf = pytest.helpers.check_ocrmypdf run_ocrmypdf = pytest.helpers.run_ocrmypdf run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api -spoof = pytest.helpers.spoof RENDERERS = ['hocr', 'sandwich'] diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 59250eac..16175140 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -47,7 +47,6 @@ pytestmark = pytest.mark.filterwarnings('ignore:.*XMLParser.*:DeprecationWarning check_ocrmypdf = pytest.helpers.check_ocrmypdf run_ocrmypdf = pytest.helpers.run_ocrmypdf -spoof = pytest.helpers.spoof @pytest.mark.parametrize("output_type", ['pdfa', 'pdf']) diff --git a/tests/test_validation.py b/tests/test_validation.py index f6fcc775..3e6272af 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -29,19 +29,29 @@ from ocrmypdf.exceptions import BadArgsError, MissingDependencyError from ocrmypdf.pdfinfo import PdfInfo -def make_opts(input_file='a.pdf', output_file='b.pdf', language='eng', **kwargs): +def make_opts_pm(input_file='a.pdf', output_file='b.pdf', language='eng', **kwargs): if language is not None: kwargs['language'] = language parser = get_parser() pm = get_plugin_manager(kwargs.get('plugins', [])) pm.hook.add_options(parser=parser) - return create_options( - input_file=input_file, output_file=output_file, parser=parser, **kwargs + return ( + create_options( + input_file=input_file, output_file=output_file, parser=parser, **kwargs + ), + pm, ) +def make_opts(*args, **kwargs): + opts, _pm = make_opts_pm(*args, **kwargs) + return opts + + def test_hocr_notlatin_warning(caplog): - vd.check_options_output(make_opts(language='chi_sim', pdf_renderer='hocr')) + vd.check_options( + *make_opts_pm(language='chi_sim', pdf_renderer='hocr', output_type='pdfa') + ) assert 'PDF renderer is known to cause' in caplog.text @@ -49,20 +59,20 @@ 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 ): - vd.check_options_output(make_opts(language='chi_sim', output_type='pdfa')) + vd.check_options(*make_opts_pm(language='chi_sim', output_type='pdfa')) assert 'Ghostscript 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 pytest.raises(MissingDependencyError): - vd.check_options_output(make_opts(output_type='pdfa-3')) + vd.check_options(*make_opts_pm(output_type='pdfa-3')) with patch('ocrmypdf.exec.ghostscript.version', return_value='9.24'), patch( 'ocrmypdf.exec.tesseract.has_textonly_pdf', return_value=True ): with pytest.raises(MissingDependencyError): - vd.check_dependency_versions(make_opts()) + vd.check_options(*make_opts_pm()) def test_old_tesseract_error(): @@ -97,7 +107,6 @@ def test_optimizing(caplog): def test_user_words(caplog): - with patch('ocrmypdf.exec.tesseract.has_user_words', return_value=False): opts = make_opts(user_words='foo') plugin_manager = get_plugin_manager(opts.plugins)