feat: implement plugin option models with backward compatibility

This commit introduces Pydantic models for plugin-specific options in OCRmyPDF, focusing on:
- Creating TesseractOptions, OptimizeOptions, and GhostscriptOptions
- Adding backward compatibility properties
- Maintaining existing CLI and API functionality
- Preparing for future plugin option registration system

The changes include:
- Added type-annotated option models with validation
- Updated OCROptions to include legacy fields
- Added backward compatibility properties for jbig2 options
- Prepared groundwork for dynamic plugin option handling

Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
James R. Barlow
2025-12-21 12:21:48 -08:00
co-authored by aider
parent 62ad37b276
commit b7640bdb9c
5 changed files with 90 additions and 5 deletions
+46 -5
View File
@@ -20,6 +20,9 @@ from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf.exceptions import BadArgsError
from ocrmypdf.helpers import monotonic
# Import plugin option models - these will be available after plugins are loaded
# We'll use forward references and handle imports dynamically
log = logging.getLogger(__name__)
PathOrIO = BinaryIO | IOBase | Path | str | bytes
@@ -137,9 +140,50 @@ class OCROptions(BaseModel):
"""Compatibility alias for jpg_quality."""
self.jpg_quality = value
# Backward compatibility properties for jbig2 options
@property
def jbig2_lossy(self):
"""Backward compatibility for jbig2_lossy."""
return getattr(self, '_jbig2_lossy', None)
@jbig2_lossy.setter
def jbig2_lossy(self, value):
"""Backward compatibility for jbig2_lossy."""
self._jbig2_lossy = value
@property
def jbig2_page_group_size(self):
"""Backward compatibility for jbig2_page_group_size."""
return getattr(self, '_jbig2_page_group_size', None)
@jbig2_page_group_size.setter
def jbig2_page_group_size(self, value):
"""Backward compatibility for jbig2_page_group_size."""
self._jbig2_page_group_size = value
@property
def jbig2_threshold(self):
"""Backward compatibility for jbig2_threshold."""
return getattr(self, '_jbig2_threshold', 0.85)
@jbig2_threshold.setter
def jbig2_threshold(self, value):
"""Backward compatibility for jbig2_threshold."""
self._jbig2_threshold = value
# Advanced options
max_image_mpixels: float = 250.0
pdf_renderer: str = 'auto'
rotate_pages_threshold: float = DEFAULT_ROTATE_PAGES_THRESHOLD
user_words: os.PathLike | None = None
user_patterns: os.PathLike | None = None
fast_web_view: float = 1.0
continue_on_soft_render_error: bool | None = None
# 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
tesseract_oem: int | None = None
@@ -148,13 +192,10 @@ class OCROptions(BaseModel):
tesseract_non_ocr_timeout: float | None = None
tesseract_downsample_above: int = 32767
tesseract_downsample_large_images: bool | None = None
rotate_pages_threshold: float = DEFAULT_ROTATE_PAGES_THRESHOLD
# Legacy ghostscript options (for backward compatibility)
pdfa_image_compression: str | None = None
color_conversion_strategy: str = "LeaveColorUnchanged"
user_words: os.PathLike | None = None
user_patterns: os.PathLike | None = None
fast_web_view: float = 1.0
continue_on_soft_render_error: bool | None = None
# Plugin system
plugins: Sequence[Path | str] | None = None
@@ -5,8 +5,10 @@
from __future__ import annotations
import logging
from typing import Annotated
from packaging.version import Version
from pydantic import BaseModel, Field
from ocrmypdf import hookimpl
from ocrmypdf._exec import ghostscript
@@ -20,6 +22,13 @@ log = logging.getLogger(__name__)
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"
@hookimpl
def add_options(parser):
gs = parser.add_argument_group("Ghostscript", "Advanced control of Ghostscript")
+14
View File
@@ -8,6 +8,9 @@ import argparse
import logging
from collections.abc import Sequence
from pathlib import Path
from typing import Annotated
from pydantic import BaseModel, Field
from ocrmypdf import Executor, PdfContext, hookimpl
from ocrmypdf._exec import jbig2enc, pngquant
@@ -19,6 +22,17 @@ from ocrmypdf.subprocess import check_external_program
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
@hookimpl
def add_options(parser):
optimizing = parser.add_argument_group(
@@ -7,8 +7,10 @@ from __future__ import annotations
import argparse
import logging
import os
from typing import Annotated
from PIL import Image
from pydantic import BaseModel, Field
from ocrmypdf import hookimpl
from ocrmypdf._exec import tesseract
@@ -23,6 +25,19 @@ from ocrmypdf.subprocess import check_external_program
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
@hookimpl
def add_options(parser):
tess = parser.add_argument_group("Tesseract", "Advanced control of Tesseract OCR")
@@ -152,6 +167,8 @@ def check_options(options):
"Please upgrade to a newer or supported older version."
)
# Validate Tesseract-specific options using the new model
# For now, we still access options directly for backward compatibility
if not tesseract.has_thresholding() and options.tesseract_thresholding != 0:
log.warning(
"The installed version of Tesseract does not support changes to its "
+4
View File
@@ -463,6 +463,10 @@ def namespace_to_options(ns) -> OCROptions:
if 'work_folder' in extra_attrs and 'input_file' not in known_fields:
known_fields['input_file'] = '/dev/null' # Placeholder
# 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