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:
@@ -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")
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user