feat: Add CLI generation methods to plugin option models
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
@@ -151,7 +151,7 @@ class OCROptions(BaseModel):
|
||||
|
||||
# Plugin option namespaces (for backward compatibility, will be removed in Phase 5)
|
||||
# These will be populated dynamically based on loaded plugins
|
||||
|
||||
|
||||
# Legacy tesseract options (for backward compatibility)
|
||||
tesseract_config: list[str] = []
|
||||
tesseract_pagesegmode: int | None = None
|
||||
@@ -161,11 +161,11 @@ class OCROptions(BaseModel):
|
||||
tesseract_non_ocr_timeout: float | None = None
|
||||
tesseract_downsample_above: int = 32767
|
||||
tesseract_downsample_large_images: bool | None = None
|
||||
|
||||
|
||||
# Legacy ghostscript options (for backward compatibility)
|
||||
pdfa_image_compression: str | None = None
|
||||
color_conversion_strategy: str = "LeaveColorUnchanged"
|
||||
|
||||
|
||||
# Legacy jbig2 options (for backward compatibility)
|
||||
jbig2_lossy: bool | None = None
|
||||
jbig2_page_group_size: int | None = None
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
from pydantic import BaseModel, create_model
|
||||
|
||||
@@ -15,59 +14,65 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class PluginOptionRegistry:
|
||||
"""Registry for plugin option models."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._option_models: Dict[str, Type[BaseModel]] = {}
|
||||
self._extended_model_cache: Type[BaseModel] | None = None
|
||||
|
||||
def register_option_model(self, namespace: str, model_class: Type[BaseModel]) -> None:
|
||||
self._option_models: dict[str, type[BaseModel]] = {}
|
||||
self._extended_model_cache: type[BaseModel] | None = None
|
||||
|
||||
def register_option_model(
|
||||
self, namespace: str, model_class: type[BaseModel]
|
||||
) -> None:
|
||||
"""Register a plugin's option model.
|
||||
|
||||
|
||||
Args:
|
||||
namespace: The namespace for the plugin options (e.g., 'tesseract')
|
||||
model_class: The Pydantic model class for the plugin options
|
||||
"""
|
||||
if namespace in self._option_models:
|
||||
log.warning(f"Plugin option namespace '{namespace}' already registered, overriding")
|
||||
|
||||
log.warning(
|
||||
f"Plugin option namespace '{namespace}' already registered, overriding"
|
||||
)
|
||||
|
||||
self._option_models[namespace] = model_class
|
||||
# Clear cache when new models are registered
|
||||
self._extended_model_cache = None
|
||||
|
||||
log.debug(f"Registered plugin option model for namespace '{namespace}': {model_class.__name__}")
|
||||
|
||||
def get_registered_models(self) -> Dict[str, Type[BaseModel]]:
|
||||
|
||||
log.debug(
|
||||
f"Registered plugin option model for namespace '{namespace}': {model_class.__name__}"
|
||||
)
|
||||
|
||||
def get_registered_models(self) -> dict[str, type[BaseModel]]:
|
||||
"""Get all registered plugin option models."""
|
||||
return self._option_models.copy()
|
||||
|
||||
def get_extended_options_model(self, base_model: Type[BaseModel]) -> Type[BaseModel]:
|
||||
|
||||
def get_extended_options_model(
|
||||
self, base_model: type[BaseModel]
|
||||
) -> type[BaseModel]:
|
||||
"""Create an extended options model that includes all registered plugin options.
|
||||
|
||||
|
||||
Args:
|
||||
base_model: The base OCROptions model to extend
|
||||
|
||||
|
||||
Returns:
|
||||
A new model class that includes the base model fields plus all plugin option fields
|
||||
"""
|
||||
if self._extended_model_cache is not None:
|
||||
return self._extended_model_cache
|
||||
|
||||
|
||||
# Start with base model fields
|
||||
model_fields = {}
|
||||
|
||||
|
||||
# Add plugin option models as nested fields
|
||||
for namespace, model_class in self._option_models.items():
|
||||
model_fields[namespace] = (model_class, model_class())
|
||||
|
||||
|
||||
# Create the extended model
|
||||
self._extended_model_cache = create_model(
|
||||
'ExtendedOCROptions',
|
||||
__base__=base_model,
|
||||
**model_fields
|
||||
'ExtendedOCROptions', __base__=base_model, **model_fields
|
||||
)
|
||||
|
||||
|
||||
return self._extended_model_cache
|
||||
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Clear the extended model cache."""
|
||||
self._extended_model_cache = None
|
||||
|
||||
+19
-21
@@ -75,53 +75,54 @@ def setup_plugin_infrastructure(
|
||||
plugin_manager: pluggy.PluginManager | None = None,
|
||||
) -> pluggy.PluginManager:
|
||||
"""Set up plugin infrastructure with proper initialization.
|
||||
|
||||
|
||||
This function handles:
|
||||
1. Creating or validating the plugin manager
|
||||
2. Calling plugin initialization hooks
|
||||
3. Setting up plugin option registry
|
||||
|
||||
|
||||
Args:
|
||||
plugins: List of plugin paths/names to load
|
||||
plugin_manager: Existing plugin manager (if any)
|
||||
|
||||
|
||||
Returns:
|
||||
Properly initialized plugin manager
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If both plugins and plugin_manager are provided
|
||||
"""
|
||||
if plugins and plugin_manager:
|
||||
raise ValueError("plugins= and plugin_manager are mutually exclusive")
|
||||
|
||||
|
||||
if not plugins:
|
||||
plugins = []
|
||||
elif isinstance(plugins, (str, Path)):
|
||||
plugins = [plugins]
|
||||
else:
|
||||
plugins = list(plugins)
|
||||
|
||||
|
||||
# Create plugin manager if not provided
|
||||
if not plugin_manager:
|
||||
plugin_manager = get_plugin_manager(plugins)
|
||||
|
||||
|
||||
# Initialize plugins
|
||||
plugin_manager.hook.initialize(plugin_manager=plugin_manager) # pylint: disable=no-member
|
||||
|
||||
|
||||
# Initialize plugin option registry
|
||||
from ocrmypdf._plugin_registry import PluginOptionRegistry
|
||||
|
||||
registry = PluginOptionRegistry()
|
||||
|
||||
|
||||
# Let plugins register their option models
|
||||
option_models = plugin_manager.hook.register_options() # pylint: disable=no-member
|
||||
for plugin_options in option_models:
|
||||
if plugin_options: # Skip None returns
|
||||
for namespace, model_class in plugin_options.items():
|
||||
registry.register_option_model(namespace, model_class)
|
||||
|
||||
|
||||
# Store registry in plugin manager for later access
|
||||
plugin_manager._option_registry = registry
|
||||
|
||||
|
||||
return plugin_manager
|
||||
|
||||
|
||||
@@ -420,10 +421,9 @@ def ocr( # noqa: D417
|
||||
with _api_lock:
|
||||
# Set up plugin infrastructure with proper initialization
|
||||
plugin_manager = setup_plugin_infrastructure(
|
||||
plugins=plugins,
|
||||
plugin_manager=plugin_manager
|
||||
plugins=plugins, plugin_manager=plugin_manager
|
||||
)
|
||||
|
||||
|
||||
# Get parser and let plugins add their options
|
||||
parser = get_parser()
|
||||
plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
|
||||
@@ -553,10 +553,9 @@ def _pdf_to_hocr( # noqa: D417
|
||||
with _api_lock:
|
||||
# Set up plugin infrastructure with proper initialization
|
||||
plugin_manager = setup_plugin_infrastructure(
|
||||
plugins=plugins,
|
||||
plugin_manager=plugin_manager
|
||||
plugins=plugins, plugin_manager=plugin_manager
|
||||
)
|
||||
|
||||
|
||||
plugin_manager.hook.add_options(parser=get_parser()) # pylint: disable=no-member
|
||||
|
||||
# Create OCROptions directly
|
||||
@@ -654,12 +653,11 @@ def _hocr_to_ocr_pdf( # noqa: D417
|
||||
extra_attrs[key] = options_kwargs.pop(key)
|
||||
|
||||
with _api_lock:
|
||||
# Set up plugin infrastructure with proper initialization
|
||||
# Set up plugin infrastructure with proper initialization
|
||||
plugin_manager = setup_plugin_infrastructure(
|
||||
plugins=plugins,
|
||||
plugin_manager=plugin_manager
|
||||
plugins=plugins, plugin_manager=plugin_manager
|
||||
)
|
||||
|
||||
|
||||
plugin_manager.hook.add_options(parser=get_parser()) # pylint: disable=no-member
|
||||
|
||||
# Create OCROptions directly
|
||||
|
||||
@@ -24,9 +24,45 @@ BLACKLISTED_GS_VERSIONS: frozenset[Version] = frozenset()
|
||||
|
||||
class GhostscriptOptions(BaseModel):
|
||||
"""Options specific to Ghostscript operations."""
|
||||
|
||||
color_conversion_strategy: Annotated[str, Field(description="Ghostscript color conversion strategy")] = "LeaveColorUnchanged"
|
||||
pdfa_image_compression: Annotated[str, Field(description="PDF/A image compression method")] = "auto"
|
||||
|
||||
color_conversion_strategy: Annotated[
|
||||
str, Field(description="Ghostscript color conversion strategy")
|
||||
] = "LeaveColorUnchanged"
|
||||
pdfa_image_compression: Annotated[
|
||||
str, Field(description="PDF/A image compression method")
|
||||
] = "auto"
|
||||
|
||||
@classmethod
|
||||
def add_arguments_to_parser(cls, parser, namespace: str = 'ghostscript'):
|
||||
"""Add Ghostscript-specific arguments to the argument parser.
|
||||
|
||||
Args:
|
||||
parser: The argument parser to add arguments to
|
||||
namespace: The namespace prefix for argument names (not used for ghostscript for backward compatibility)
|
||||
"""
|
||||
gs = parser.add_argument_group("Ghostscript", "Advanced control of Ghostscript")
|
||||
gs.add_argument(
|
||||
'--color-conversion-strategy',
|
||||
action='store',
|
||||
type=str,
|
||||
metavar='STRATEGY',
|
||||
choices=ghostscript.COLOR_CONVERSION_STRATEGIES,
|
||||
default='LeaveColorUnchanged',
|
||||
help="Set Ghostscript color conversion strategy",
|
||||
)
|
||||
gs.add_argument(
|
||||
'--pdfa-image-compression',
|
||||
choices=['auto', 'jpeg', 'lossless'],
|
||||
default='auto',
|
||||
help="Specify how to compress images in the output PDF/A. 'auto' lets "
|
||||
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
|
||||
"JPEG compression. 'lossless' uses PNG-style lossless compression "
|
||||
"for all images. Monochrome images are always compressed using a "
|
||||
"lossless codec. Compression settings "
|
||||
"are applied to all pages, including those for which OCR was "
|
||||
"skipped. Not supported for --output-type=pdf ; that setting "
|
||||
"preserves the original compression of all images.",
|
||||
)
|
||||
|
||||
|
||||
@hookimpl
|
||||
@@ -37,29 +73,8 @@ def register_options():
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
gs = parser.add_argument_group("Ghostscript", "Advanced control of Ghostscript")
|
||||
gs.add_argument(
|
||||
'--color-conversion-strategy',
|
||||
action='store',
|
||||
type=str,
|
||||
metavar='STRATEGY',
|
||||
choices=ghostscript.COLOR_CONVERSION_STRATEGIES,
|
||||
default='LeaveColorUnchanged',
|
||||
help="Set Ghostscript color conversion strategy",
|
||||
)
|
||||
gs.add_argument(
|
||||
'--pdfa-image-compression',
|
||||
choices=['auto', 'jpeg', 'lossless'],
|
||||
default='auto',
|
||||
help="Specify how to compress images in the output PDF/A. 'auto' lets "
|
||||
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
|
||||
"JPEG compression. 'lossless' uses PNG-style lossless compression "
|
||||
"for all images. Monochrome images are always compressed using a "
|
||||
"lossless codec. Compression settings "
|
||||
"are applied to all pages, including those for which OCR was "
|
||||
"skipped. Not supported for --output-type=pdf ; that setting "
|
||||
"preserves the original compression of all images.",
|
||||
)
|
||||
# Use the model's CLI generation method
|
||||
GhostscriptOptions.add_arguments_to_parser(parser)
|
||||
|
||||
|
||||
@hookimpl
|
||||
|
||||
@@ -24,13 +24,120 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class OptimizeOptions(BaseModel):
|
||||
"""Options specific to PDF optimization."""
|
||||
|
||||
level: Annotated[int, Field(ge=0, le=3, description="Optimization level (0=none, 1=safe, 2=lossy, 3=aggressive)")] = 1
|
||||
jpeg_quality: Annotated[int, Field(ge=0, le=100, description="JPEG quality level for optimization")] = 0
|
||||
png_quality: Annotated[int, Field(ge=0, le=100, description="PNG quality level for optimization")] = 0
|
||||
jbig2_lossy: Annotated[bool, Field(description="Enable JBIG2 lossy compression")] = False
|
||||
jbig2_page_group_size: Annotated[int, Field(ge=1, le=10000, description="Number of pages to consider for JBIG2 compression")] = 0
|
||||
jbig2_threshold: Annotated[float, Field(ge=0.4, le=0.9, description="JBIG2 symbol classification threshold")] = 0.85
|
||||
|
||||
level: Annotated[
|
||||
int,
|
||||
Field(
|
||||
ge=0,
|
||||
le=3,
|
||||
description="Optimization level (0=none, 1=safe, 2=lossy, 3=aggressive)",
|
||||
),
|
||||
] = 1
|
||||
jpeg_quality: Annotated[
|
||||
int, Field(ge=0, le=100, description="JPEG quality level for optimization")
|
||||
] = 0
|
||||
png_quality: Annotated[
|
||||
int, Field(ge=0, le=100, description="PNG quality level for optimization")
|
||||
] = 0
|
||||
jbig2_lossy: Annotated[
|
||||
bool, Field(description="Enable JBIG2 lossy compression")
|
||||
] = False
|
||||
jbig2_page_group_size: Annotated[
|
||||
int,
|
||||
Field(
|
||||
ge=1,
|
||||
le=10000,
|
||||
description="Number of pages to consider for JBIG2 compression",
|
||||
),
|
||||
] = 0
|
||||
jbig2_threshold: Annotated[
|
||||
float,
|
||||
Field(ge=0.4, le=0.9, description="JBIG2 symbol classification threshold"),
|
||||
] = 0.85
|
||||
|
||||
@classmethod
|
||||
def add_arguments_to_parser(cls, parser, namespace: str = 'optimize'):
|
||||
"""Add optimization-specific arguments to the argument parser.
|
||||
|
||||
Args:
|
||||
parser: The argument parser to add arguments to
|
||||
namespace: The namespace prefix for argument names (not used for optimize for backward compatibility)
|
||||
"""
|
||||
optimizing = parser.add_argument_group(
|
||||
"Optimization options", "Control how the PDF is optimized after OCR"
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'-O',
|
||||
'--optimize',
|
||||
type=int,
|
||||
choices=range(0, 4),
|
||||
default=1,
|
||||
help=(
|
||||
"Control how PDF is optimized after processing:"
|
||||
"0 - do not optimize; "
|
||||
"1 - do safe, lossless optimizations (default); "
|
||||
"2 - do lossy JPEG and JPEG2000 optimizations; "
|
||||
"3 - do more aggressive lossy JPEG and JPEG2000 optimizations. "
|
||||
"To enable lossy JBIG2, see --jbig2-lossy."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jpeg-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
help=(
|
||||
"Adjust JPEG quality level for JPEG optimization. "
|
||||
"100 is best quality and largest output size; "
|
||||
"1 is lowest quality and smallest output; "
|
||||
"0 uses the default."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jpg-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
dest='jpeg_quality',
|
||||
help=argparse.SUPPRESS, # Alias for --jpeg-quality
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--png-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
help=(
|
||||
"Adjust PNG quality level to use when quantizing PNGs. "
|
||||
"Values have same meaning as with --jpeg-quality"
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-lossy',
|
||||
action='store_true',
|
||||
help=(
|
||||
"Enable JBIG2 lossy mode (better compression, not suitable for some "
|
||||
"use cases - see documentation). Only takes effect if --optimize 1 or "
|
||||
"higher is also enabled."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-page-group-size',
|
||||
type=numeric(int, 1, 10000),
|
||||
default=0,
|
||||
metavar='N',
|
||||
# Adjust number of pages to consider at once for JBIG2 compression
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-threshold',
|
||||
type=numeric(float, 0.4, 0.9),
|
||||
default=0.85,
|
||||
metavar='T',
|
||||
help=(
|
||||
"Adjust JBIG2 symbol code classification threshold "
|
||||
"(default 0.85), range 0.4 to 0.9."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@hookimpl
|
||||
@@ -41,81 +148,8 @@ def register_options():
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
optimizing = parser.add_argument_group(
|
||||
"Optimization options", "Control how the PDF is optimized after OCR"
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'-O',
|
||||
'--optimize',
|
||||
type=int,
|
||||
choices=range(0, 4),
|
||||
default=1,
|
||||
help=(
|
||||
"Control how PDF is optimized after processing:"
|
||||
"0 - do not optimize; "
|
||||
"1 - do safe, lossless optimizations (default); "
|
||||
"2 - do lossy JPEG and JPEG2000 optimizations; "
|
||||
"3 - do more aggressive lossy JPEG and JPEG2000 optimizations. "
|
||||
"To enable lossy JBIG2, see --jbig2-lossy."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jpeg-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
help=(
|
||||
"Adjust JPEG quality level for JPEG optimization. "
|
||||
"100 is best quality and largest output size; "
|
||||
"1 is lowest quality and smallest output; "
|
||||
"0 uses the default."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jpg-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
dest='jpeg_quality',
|
||||
help=argparse.SUPPRESS, # Alias for --jpeg-quality
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--png-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
help=(
|
||||
"Adjust PNG quality level to use when quantizing PNGs. "
|
||||
"Values have same meaning as with --jpeg-quality"
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-lossy',
|
||||
action='store_true',
|
||||
help=(
|
||||
"Enable JBIG2 lossy mode (better compression, not suitable for some "
|
||||
"use cases - see documentation). Only takes effect if --optimize 1 or "
|
||||
"higher is also enabled."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-page-group-size',
|
||||
type=numeric(int, 1, 10000),
|
||||
default=0,
|
||||
metavar='N',
|
||||
# Adjust number of pages to consider at once for JBIG2 compression
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-threshold',
|
||||
type=numeric(float, 0.4, 0.9),
|
||||
default=0.85,
|
||||
metavar='T',
|
||||
help=(
|
||||
"Adjust JBIG2 symbol code classification threshold "
|
||||
"(default 0.85), range 0.4 to 0.9."
|
||||
),
|
||||
)
|
||||
# Use the model's CLI generation method
|
||||
OptimizeOptions.add_arguments_to_parser(parser)
|
||||
|
||||
|
||||
@hookimpl
|
||||
|
||||
@@ -27,15 +27,160 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class TesseractOptions(BaseModel):
|
||||
"""Options specific to Tesseract OCR engine."""
|
||||
|
||||
config: Annotated[list[str], Field(description="Additional Tesseract configuration files")] = []
|
||||
pagesegmode: Annotated[int | None, Field(ge=0, le=13, description="Set Tesseract page segmentation mode")] = None
|
||||
oem: Annotated[int | None, Field(ge=0, le=3, description="Set Tesseract OCR engine mode")] = None
|
||||
thresholding: Annotated[int | None, Field(description="Set Tesseract input image thresholding mode")] = None
|
||||
timeout: Annotated[float, Field(ge=0, description="Timeout for OCR operations in seconds")] = 180.0
|
||||
non_ocr_timeout: Annotated[float, Field(ge=0, description="Timeout for non-OCR operations in seconds")] = 180.0
|
||||
downsample_large_images: Annotated[bool, Field(description="Downsample large images before OCR")] = True
|
||||
downsample_above: Annotated[int, Field(ge=100, le=32767, description="Downsample images larger than this pixel size")] = 32767
|
||||
|
||||
config: Annotated[
|
||||
list[str], Field(description="Additional Tesseract configuration files")
|
||||
] = []
|
||||
pagesegmode: Annotated[
|
||||
int | None,
|
||||
Field(ge=0, le=13, description="Set Tesseract page segmentation mode"),
|
||||
] = None
|
||||
oem: Annotated[
|
||||
int | None, Field(ge=0, le=3, description="Set Tesseract OCR engine mode")
|
||||
] = None
|
||||
thresholding: Annotated[
|
||||
int | None, Field(description="Set Tesseract input image thresholding mode")
|
||||
] = None
|
||||
timeout: Annotated[
|
||||
float, Field(ge=0, description="Timeout for OCR operations in seconds")
|
||||
] = 180.0
|
||||
non_ocr_timeout: Annotated[
|
||||
float, Field(ge=0, description="Timeout for non-OCR operations in seconds")
|
||||
] = 180.0
|
||||
downsample_large_images: Annotated[
|
||||
bool, Field(description="Downsample large images before OCR")
|
||||
] = True
|
||||
downsample_above: Annotated[
|
||||
int,
|
||||
Field(
|
||||
ge=100,
|
||||
le=32767,
|
||||
description="Downsample images larger than this pixel size",
|
||||
),
|
||||
] = 32767
|
||||
|
||||
@classmethod
|
||||
def add_arguments_to_parser(cls, parser, namespace: str = 'tesseract'):
|
||||
"""Add Tesseract-specific arguments to the argument parser.
|
||||
|
||||
Args:
|
||||
parser: The argument parser to add arguments to
|
||||
namespace: The namespace prefix for argument names
|
||||
"""
|
||||
tess = parser.add_argument_group(
|
||||
"Tesseract", "Advanced control of Tesseract OCR"
|
||||
)
|
||||
|
||||
tess.add_argument(
|
||||
f'--{namespace}-config',
|
||||
action='append',
|
||||
metavar='CFG',
|
||||
default=[],
|
||||
dest=f'{namespace}_config',
|
||||
help="Additional Tesseract configuration files -- see documentation.",
|
||||
)
|
||||
|
||||
tess.add_argument(
|
||||
f'--{namespace}-pagesegmode',
|
||||
action='store',
|
||||
type=int,
|
||||
metavar='PSM',
|
||||
choices=range(0, 14),
|
||||
dest=f'{namespace}_pagesegmode',
|
||||
help="Set Tesseract page segmentation mode (see tesseract --help).",
|
||||
)
|
||||
|
||||
tess.add_argument(
|
||||
f'--{namespace}-oem',
|
||||
action='store',
|
||||
type=int,
|
||||
metavar='MODE',
|
||||
choices=range(0, 4),
|
||||
dest=f'{namespace}_oem',
|
||||
help=(
|
||||
"Set Tesseract 4+ OCR engine mode: "
|
||||
"0 - original Tesseract only; "
|
||||
"1 - neural nets LSTM only; "
|
||||
"2 - Tesseract + LSTM; "
|
||||
"3 - default."
|
||||
),
|
||||
)
|
||||
|
||||
tess.add_argument(
|
||||
f'--{namespace}-thresholding',
|
||||
action='store',
|
||||
type=str_to_int(tesseract.TESSERACT_THRESHOLDING_METHODS),
|
||||
default='auto',
|
||||
metavar='METHOD',
|
||||
dest=f'{namespace}_thresholding',
|
||||
help=(
|
||||
"Set Tesseract 5.0+ input image thresholding mode. This may improve OCR "
|
||||
"results on low quality images or those that contain high contrast color. "
|
||||
"legacy-otsu is the Tesseract default; adaptive-otsu is an improved Otsu "
|
||||
"algorithm with improved sort for background color changes; sauvola is "
|
||||
"based on local standard deviation."
|
||||
),
|
||||
)
|
||||
|
||||
tess.add_argument(
|
||||
f'--{namespace}-timeout',
|
||||
default=180.0,
|
||||
type=numeric(float, 0),
|
||||
metavar='SECONDS',
|
||||
dest=f'{namespace}_timeout',
|
||||
help=(
|
||||
"Give up on OCR after the timeout, but copy the preprocessed page "
|
||||
"into the final output. This timeout is only used when using Tesseract "
|
||||
"for OCR. When Tesseract is used for other operations such as "
|
||||
"deskewing and orientation, the timeout is controlled by "
|
||||
f"--{namespace}-non-ocr-timeout."
|
||||
),
|
||||
)
|
||||
|
||||
tess.add_argument(
|
||||
f'--{namespace}-non-ocr-timeout',
|
||||
default=180.0,
|
||||
type=numeric(float, 0),
|
||||
metavar='SECONDS',
|
||||
dest=f'{namespace}_non_ocr_timeout',
|
||||
help=(
|
||||
"Give up on non-OCR operations such as deskewing and orientation "
|
||||
f"after timeout. This is a separate timeout from --{namespace}-timeout "
|
||||
"because these operations are not as expensive as OCR."
|
||||
),
|
||||
)
|
||||
|
||||
tess.add_argument(
|
||||
f'--{namespace}-downsample-large-images',
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
dest=f'{namespace}_downsample_large_images',
|
||||
help=(
|
||||
"Downsample large images before OCR. Tesseract has an upper limit on the "
|
||||
"size images it will support. If this argument is given, OCRmyPDF will "
|
||||
"downsample large images to fit Tesseract. This may reduce OCR quality, "
|
||||
"on large images the most desirable text is usually larger. If this "
|
||||
"parameter is not supplied, Tesseract will error out and produce no OCR "
|
||||
"on the page in question. This argument should be used with a high value "
|
||||
f"of --{namespace}-timeout to ensure Tesseract has enough to time."
|
||||
),
|
||||
)
|
||||
|
||||
tess.add_argument(
|
||||
f'--{namespace}-downsample-above',
|
||||
action='store',
|
||||
type=numeric(int, 100, 32767),
|
||||
default=32767,
|
||||
dest=f'{namespace}_downsample_above',
|
||||
help=(
|
||||
"Downsample images larger than this size pixel size in either dimension "
|
||||
f"before OCR. --{namespace}-downsample-large-images downsamples only when "
|
||||
"an image exceeds Tesseract's internal limits. This argument causes "
|
||||
"downsampling to occur when an image exceeds the given size. This may "
|
||||
"reduce OCR quality, but on large images the most desirable text is "
|
||||
"usually larger."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@hookimpl
|
||||
@@ -46,102 +191,11 @@ def register_options():
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
# Use the model's CLI generation method
|
||||
TesseractOptions.add_arguments_to_parser(parser)
|
||||
|
||||
# Add user words and patterns (these are not part of TesseractOptions model yet)
|
||||
tess = parser.add_argument_group("Tesseract", "Advanced control of Tesseract OCR")
|
||||
tess.add_argument(
|
||||
'--tesseract-config',
|
||||
action='append',
|
||||
metavar='CFG',
|
||||
default=[],
|
||||
help="Additional Tesseract configuration files -- see documentation.",
|
||||
)
|
||||
tess.add_argument(
|
||||
'--tesseract-pagesegmode',
|
||||
action='store',
|
||||
type=int,
|
||||
metavar='PSM',
|
||||
choices=range(0, 14),
|
||||
help="Set Tesseract page segmentation mode (see tesseract --help).",
|
||||
)
|
||||
tess.add_argument(
|
||||
'--tesseract-oem',
|
||||
action='store',
|
||||
type=int,
|
||||
metavar='MODE',
|
||||
choices=range(0, 4),
|
||||
help=(
|
||||
"Set Tesseract 4+ OCR engine mode: "
|
||||
"0 - original Tesseract only; "
|
||||
"1 - neural nets LSTM only; "
|
||||
"2 - Tesseract + LSTM; "
|
||||
"3 - default."
|
||||
),
|
||||
)
|
||||
tess.add_argument(
|
||||
'--tesseract-thresholding',
|
||||
action='store',
|
||||
type=str_to_int(tesseract.TESSERACT_THRESHOLDING_METHODS),
|
||||
default='auto',
|
||||
metavar='METHOD',
|
||||
help=(
|
||||
"Set Tesseract 5.0+ input image thresholding mode. This may improve OCR "
|
||||
"results on low quality images or those that contain high contrast color. "
|
||||
"legacy-otsu is the Tesseract default; adaptive-otsu is an improved Otsu "
|
||||
"algorithm with improved sort for background color changes; sauvola is "
|
||||
"based on local standard deviation."
|
||||
),
|
||||
)
|
||||
tess.add_argument(
|
||||
'--tesseract-timeout',
|
||||
default=180.0,
|
||||
type=numeric(float, 0),
|
||||
metavar='SECONDS',
|
||||
help=(
|
||||
"Give up on OCR after the timeout, but copy the preprocessed page "
|
||||
"into the final output. This timeout is only used when using Tesseract "
|
||||
"for OCR. When Tesseract is used for other operations such as "
|
||||
"deskewing and orientation, the timeout is controlled by "
|
||||
"--tesseract-non-ocr-timeout."
|
||||
),
|
||||
)
|
||||
tess.add_argument(
|
||||
'--tesseract-non-ocr-timeout',
|
||||
default=180.0,
|
||||
type=numeric(float, 0),
|
||||
metavar='SECONDS',
|
||||
help=(
|
||||
"Give up on non-OCR operations such as deskewing and orientation "
|
||||
"after timeout. This is a separate timeout from --tesseract-timeout "
|
||||
"because these operations are not as expensive as OCR."
|
||||
),
|
||||
)
|
||||
tess.add_argument(
|
||||
'--tesseract-downsample-large-images',
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help=(
|
||||
"Downsample large images before OCR. Tesseract has an upper limit on the "
|
||||
"size images it will support. If this argument is given, OCRmyPDF will "
|
||||
"downsample large images to fit Tesseract. This may reduce OCR quality, "
|
||||
"on large images the most desirable text is usually larger. If this "
|
||||
"parameter is not supplied, Tesseract will error out and produce no OCR "
|
||||
"on the page in question. This argument should be used with a high value "
|
||||
"of --tesseract-timeout to ensure Tesseract has enough to time."
|
||||
),
|
||||
)
|
||||
tess.add_argument(
|
||||
'--tesseract-downsample-above',
|
||||
action='store',
|
||||
type=numeric(int, 100, 32767),
|
||||
default=32767,
|
||||
help=(
|
||||
"Downsample images larger than this size pixel size in either dimension "
|
||||
"before OCR. --tesseract-downsample-large-images downsamples only when "
|
||||
"an image exceeds Tesseract's internal limits. This argument causes "
|
||||
"downsampling to occur when an image exceeds the given size. This may "
|
||||
"reduce OCR quality, but on large images the most desirable text is "
|
||||
"usually larger."
|
||||
),
|
||||
)
|
||||
tess.add_argument(
|
||||
'--user-words',
|
||||
metavar='FILE',
|
||||
|
||||
+3
-4
@@ -15,7 +15,6 @@ import pluggy
|
||||
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
|
||||
from ocrmypdf._defaults import PROGRAM_NAME as _PROGRAM_NAME
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||
from ocrmypdf._version import __version__ as _VERSION
|
||||
|
||||
T = TypeVar('T', int, float)
|
||||
@@ -466,7 +465,7 @@ def namespace_to_options(ns) -> OCROptions:
|
||||
# Handle backward compatibility for plugin options
|
||||
# Map CLI arguments to the appropriate fields for now
|
||||
# In Phase 2, this will be handled by plugin option models
|
||||
|
||||
|
||||
instance = OCROptions(**known_fields)
|
||||
instance.extra_attrs = extra_attrs
|
||||
return instance
|
||||
@@ -489,10 +488,10 @@ def get_options_and_plugins(
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from ocrmypdf.api import setup_plugin_infrastructure
|
||||
|
||||
|
||||
# First pass: get plugins so we can register their options
|
||||
pre_options, _unused = plugins_only_parser.parse_known_args(args=args)
|
||||
|
||||
|
||||
# Set up plugin infrastructure with proper initialization
|
||||
plugin_manager = setup_plugin_infrastructure(plugins=pre_options.plugins)
|
||||
|
||||
|
||||
@@ -91,19 +91,19 @@ def add_options(parser: ArgumentParser) -> None:
|
||||
@hookspec
|
||||
def register_options() -> dict[str, type[BaseModel]]:
|
||||
"""Return plugin's option models keyed by namespace.
|
||||
|
||||
|
||||
This hook allows plugins to register their option models with the
|
||||
plugin option registry. The returned dictionary should map namespace
|
||||
strings to Pydantic model classes.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping namespace strings to BaseModel classes
|
||||
|
||||
|
||||
Example:
|
||||
@hookimpl
|
||||
def register_options():
|
||||
return {'tesseract': TesseractOptions}
|
||||
|
||||
|
||||
Note:
|
||||
This hook will be called from the main process during plugin
|
||||
infrastructure setup, before child worker processes are forked.
|
||||
|
||||
@@ -15,7 +15,6 @@ from pikepdf.models.metadata import decode_pdf_date
|
||||
from ocrmypdf._jobcontext import PdfContext
|
||||
from ocrmypdf._metadata import metadata_fixup
|
||||
from ocrmypdf._pipeline import convert_to_pdfa
|
||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||
from ocrmypdf.api import setup_plugin_infrastructure
|
||||
from ocrmypdf.cli import get_options_and_plugins
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
@@ -333,9 +332,7 @@ def test_metadata_fixup_warning(resources, outdir, caplog):
|
||||
|
||||
# Use the new setup function instead of get_plugin_manager directly
|
||||
plugin_manager = setup_plugin_infrastructure([])
|
||||
context = PdfContext(
|
||||
options, outdir, outdir / 'graph.pdf', None, plugin_manager
|
||||
)
|
||||
context = PdfContext(options, outdir, outdir / 'graph.pdf', None, plugin_manager)
|
||||
metadata_fixup(
|
||||
working_file=outdir / 'graph.pdf', context=context, pdf_save_settings={}
|
||||
)
|
||||
@@ -382,7 +379,7 @@ def test_prevent_gs_invalid_xml(resources, outdir):
|
||||
]
|
||||
)
|
||||
pdfinfo = PdfInfo(outdir / 'layers.rendered.pdf')
|
||||
|
||||
|
||||
# Use the new setup function
|
||||
plugin_manager = setup_plugin_infrastructure([])
|
||||
context = PdfContext(
|
||||
|
||||
@@ -12,7 +12,6 @@ from packaging.version import Version
|
||||
|
||||
from ocrmypdf._exec import unpaper
|
||||
from ocrmypdf._validation import check_options
|
||||
from ocrmypdf.api import setup_plugin_infrastructure
|
||||
from ocrmypdf.cli import get_options_and_plugins
|
||||
from ocrmypdf.exceptions import BadArgsError, ExitCode, MissingDependencyError
|
||||
|
||||
|
||||
Reference in New Issue
Block a user