diff --git a/setup.cfg b/setup.cfg
index f307a2e5..3cb3db9d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -23,7 +23,7 @@ force_grid_wrap=0
use_parentheses=True
line_length=88
known_first_party = ocrmypdf
-known_third_party = PIL,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
+known_third_party = PIL,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pluggy,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
[metadata]
license_file = LICENSE
diff --git a/src/ocrmypdf/__init__.py b/src/ocrmypdf/__init__.py
index 2f76bf4f..2326efb2 100644
--- a/src/ocrmypdf/__init__.py
+++ b/src/ocrmypdf/__init__.py
@@ -15,10 +15,13 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see .
-from . import helpers, hocrtransform, leptonica, pdfa, pdfinfo
-from ._version import PROGRAM_NAME, __version__
-from .api import Verbosity, configure_logging, ocr
-from .exceptions import (
+
+from pluggy import HookimplMarker
+
+from ocrmypdf import helpers, hocrtransform, leptonica, pdfa, pdfinfo
+from ocrmypdf._version import PROGRAM_NAME, __version__
+from ocrmypdf.api import Verbosity, configure_logging, ocr
+from ocrmypdf.exceptions import (
BadArgsError,
DpiError,
EncryptedPdfError,
@@ -33,3 +36,5 @@ from .exceptions import (
TesseractConfigError,
UnsupportedImageFormatError,
)
+
+hookimpl = HookimplMarker('ocrmypdf')
diff --git a/src/ocrmypdf/_jobcontext.py b/src/ocrmypdf/_jobcontext.py
index a782d596..eac75d5e 100644
--- a/src/ocrmypdf/_jobcontext.py
+++ b/src/ocrmypdf/_jobcontext.py
@@ -24,11 +24,12 @@ import sys
class PDFContext:
"""Holds our context for a particular run of the pipeline"""
- def __init__(self, options, work_folder, origin, pdfinfo):
+ def __init__(self, options, work_folder, origin, pdfinfo, plugin_manager):
self.options = options
self.work_folder = work_folder
self.origin = origin
self.pdfinfo = pdfinfo
+ self.plugin_manager = plugin_manager
if options:
self.name = os.path.basename(options.input_file)
else:
diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py
index 2128adf3..6a691b81 100644
--- a/src/ocrmypdf/_pipeline.py
+++ b/src/ocrmypdf/_pipeline.py
@@ -194,6 +194,7 @@ def validate_pdfinfo_options(context):
"form and all filled form fields. The output PDF will be "
"'flattened' and will no longer be fillable."
)
+ context.plugin_manager.hook.prepare(options=options)
def get_page_dpi(pageinfo, options):
diff --git a/src/ocrmypdf/_pluginspec.py b/src/ocrmypdf/_pluginspec.py
new file mode 100644
index 00000000..c3aac6f2
--- /dev/null
+++ b/src/ocrmypdf/_pluginspec.py
@@ -0,0 +1,66 @@
+# © 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 .
+
+from argparse import Namespace
+
+import pluggy
+from PIL import Image
+
+from ocrmypdf.pdfinfo import PdfInfo
+
+hookspec = pluggy.HookspecMarker('ocrmypdf')
+
+# pylint: disable=unused-argument
+
+
+@hookspec
+def prepare(options: Namespace) -> None:
+ """Called to notify a plugin that a file will be processed.
+
+ The plugin may modify the options. All objects that are in options must
+ be picklable so they can be marshalled to child worker processes.
+
+ Typically, a plugin will call ``registry.register_plugin(__name__)`` to register
+ all of its public functions with the plugin registry. Functions that are
+ not intended for registration should be prefixed with an underscore.
+ Functions that imported from other modules will be ignored by
+ ``.register_plugin()``. For example if you use ``from os import basename``,
+ ``basename`` will not be registered.
+ """
+
+
+@hookspec
+def validate(pdfinfo: PdfInfo, options: Namespace) -> None:
+ """Called to give a plugin an opportunity to review options and pdfinfo.
+
+ options contains the "work order" to process a particular file. pdfinfo
+ contains information about the input file obtained after loading and
+ parsing.
+
+ The plugin may raise InputFileError or any ExitCodeException to request
+ normal termination. If the plugin raises another exception type, ocrmypdf
+ will abort with an error and hold the plugin responsible.
+ """
+
+
+@hookspec
+def filter_ocr_image(image: Image) -> Image:
+ """Called to filter the image before it is sent to OCR.
+
+ This is the image that OCR sees, not what the user sees when they view the
+ PDF.
+ """
diff --git a/src/ocrmypdf/_sync.py b/src/ocrmypdf/_sync.py
index 77ecfa9b..69da25bc 100644
--- a/src/ocrmypdf/_sync.py
+++ b/src/ocrmypdf/_sync.py
@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see .
+import importlib
import logging
import logging.handlers
import multiprocessing
@@ -28,12 +29,14 @@ from pathlib import Path
from tempfile import mkdtemp
import PIL
+import pluggy
-from ._concurrent import exec_progress_pool
-from ._graft import OcrGrafter
-from ._jobcontext import PDFContext, cleanup_working_files
-from ._logging import PageNumberFilter
-from ._pipeline import (
+from ocrmypdf import _pluginspec
+from ocrmypdf._concurrent import exec_progress_pool
+from ocrmypdf._graft import OcrGrafter
+from ocrmypdf._jobcontext import PDFContext, cleanup_working_files
+from ocrmypdf._logging import PageNumberFilter
+from ocrmypdf._pipeline import (
convert_to_pdfa,
copy_final,
create_ocr_image,
@@ -58,14 +61,14 @@ from ._pipeline import (
triage,
validate_pdfinfo_options,
)
-from ._validation import (
+from ocrmypdf._validation import (
check_requested_output_file,
create_input_file,
report_output_file_size,
)
-from .exceptions import ExitCode, ExitCodeException
-from .helpers import available_cpu_count, check_pdf
-from .pdfa import file_claims_pdfa
+from ocrmypdf.exceptions import ExitCode, ExitCodeException
+from ocrmypdf.helpers import available_cpu_count, check_pdf
+from ocrmypdf.pdfa import file_claims_pdfa
log = logging.getLogger(__name__)
@@ -298,6 +301,24 @@ def configure_debug_logging(log_filename, prefix=''):
return log_file_handler
+def _load_object_from_module(location):
+ """Load a object given a module location
+
+ For location=a.b.c, will effectively run "from a.b import c"
+
+ Example:
+ _load_object_from_module("a.b.c")
+
+ """
+ module_parts = location.split('.')
+ module_name = '.'.join(module_parts[:-1])
+ object_name = module_parts[-1]
+ module = importlib.import_module(module_name)
+ obj = getattr(module, object_name)
+ log.debug(f"Loaded object: from {module_name} import {object_name}")
+ return obj
+
+
def run_pipeline(options, api=False):
# Any changes to options will not take effect for options that are already
# bound to function parameters in the pipeline. (For example
@@ -312,6 +333,14 @@ def run_pipeline(options, api=False):
):
debug_log_handler = configure_debug_logging(Path(work_folder) / "debug.log")
+ pm = pluggy.PluginManager('ocrmypdf')
+ pm.add_hookspecs(_pluginspec)
+
+ for name in options.plugins:
+ # module = _load_object_from_module(name)
+ module = importlib.import_module(name)
+ pm.register(module)
+
try:
check_requested_output_file(options)
start_input_file, original_filename = create_input_file(options, work_folder)
@@ -331,7 +360,7 @@ def run_pipeline(options, api=False):
max_workers=options.jobs if not options.use_threads else 1, # To help debug
)
- context = PDFContext(options, work_folder, origin_pdf, pdfinfo)
+ context = PDFContext(options, work_folder, origin_pdf, pdfinfo, pm)
# Validate options are okay for this pdf
validate_pdfinfo_options(context)
diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py
index e28162e7..48a90339 100644
--- a/src/ocrmypdf/cli.py
+++ b/src/ocrmypdf/cli.py
@@ -480,6 +480,12 @@ advanced.add_argument(
"which do not benefit. If the threshold is 0 it will be apply to all files. "
"Set the threshold very high to disable.",
)
+advanced.add_argument(
+ '--plugins',
+ action='append',
+ default=[],
+ help="Path to a folder than contains plugins.",
+)
debugging = parser.add_argument_group(
"Debugging", "Arguments to help with troubleshooting and debugging"
diff --git a/src/ocrmypdf/example.py b/src/ocrmypdf/example.py
new file mode 100644
index 00000000..a890bb3e
--- /dev/null
+++ b/src/ocrmypdf/example.py
@@ -0,0 +1,6 @@
+import ocrmypdf
+
+
+@ocrmypdf.hookimpl
+def prepare(options):
+ raise ValueError('foo')