diff --git a/src/ocrmypdf/_options.py b/src/ocrmypdf/_options.py index 04d012c3..8eb6fdc1 100644 --- a/src/ocrmypdf/_options.py +++ b/src/ocrmypdf/_options.py @@ -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 diff --git a/src/ocrmypdf/builtin_plugins/ghostscript.py b/src/ocrmypdf/builtin_plugins/ghostscript.py index d5fbd92f..550ac599 100644 --- a/src/ocrmypdf/builtin_plugins/ghostscript.py +++ b/src/ocrmypdf/builtin_plugins/ghostscript.py @@ -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") diff --git a/src/ocrmypdf/builtin_plugins/optimize.py b/src/ocrmypdf/builtin_plugins/optimize.py index ff841f93..e5e3ffc6 100644 --- a/src/ocrmypdf/builtin_plugins/optimize.py +++ b/src/ocrmypdf/builtin_plugins/optimize.py @@ -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( diff --git a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py index 765647b5..1f8c94e4 100644 --- a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py +++ b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py @@ -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 " diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py index f7552177..c097d026 100644 --- a/src/ocrmypdf/cli.py +++ b/src/ocrmypdf/cli.py @@ -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