Rename OCROptions to OcrOptions for consistency
Technically OCROptions is more Pythonic but we have several pre-existing classes named OcrWhatever. Go with the local flow.
This commit is contained in:
+4
-4
@@ -122,10 +122,10 @@ When OCRmyPDF succeeds conditionally, it returns an integer exit code.
|
||||
|
||||
Starting in OCRmyPDF v16.13.0, the plugin interface has been updated:
|
||||
|
||||
- Plugin hooks now receive `OCROptions` objects instead of `argparse.Namespace`
|
||||
- `OCROptions` provides the same attribute access as `Namespace` (duck-typing compatible)
|
||||
- Plugin developers should update type hints: `from ocrmypdf._options import OCROptions`
|
||||
- Plugin hooks now receive `OcrOptions` objects instead of `argparse.Namespace`
|
||||
- `OcrOptions` provides the same attribute access as `Namespace` (duck-typing compatible)
|
||||
- Plugin developers should update type hints: `from ocrmypdf._options import OcrOptions`
|
||||
- Built-in plugins no longer modify options in-place for better immutability
|
||||
|
||||
Most existing plugins will continue working without modification due to the
|
||||
duck-typing compatibility between `OCROptions` and `Namespace`.
|
||||
duck-typing compatibility between `OcrOptions` and `Namespace`.
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ should be mainly of interest to plugin developers.
|
||||
|
||||
```{eval-rst}
|
||||
.. automodule:: ocrmypdf._options
|
||||
:members: OCROptions
|
||||
:members: OcrOptions
|
||||
```
|
||||
|
||||
## ocrmypdf.exceptions
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ Both access patterns are equivalent and return the same values.
|
||||
|
||||
:::{note}
|
||||
**Plugin Interface Change**: Starting in OCRmyPDF v16.13.0, plugin hooks receive
|
||||
`OCROptions` objects instead of `argparse.Namespace` objects. Most plugins will
|
||||
`OcrOptions` objects instead of `argparse.Namespace` objects. Most plugins will
|
||||
continue working due to duck-typing compatibility, but plugin developers should
|
||||
update their type hints accordingly.
|
||||
:::
|
||||
|
||||
@@ -29,24 +29,24 @@ official when it's tagged and posted to PyPI.
|
||||
|
||||
**Breaking changes**
|
||||
|
||||
- **Plugin interface migration**: Plugin hooks now receive `OCROptions` objects instead of
|
||||
- **Plugin interface migration**: Plugin hooks now receive `OcrOptions` objects instead of
|
||||
`argparse.Namespace` objects. Most plugins will continue working due to duck-typing
|
||||
compatibility, but plugin developers should update their type hints from `Namespace`
|
||||
to `OCROptions`.
|
||||
to `OcrOptions`.
|
||||
- Built-in plugins no longer modify options in-place, improving immutability and
|
||||
code clarity.
|
||||
|
||||
**API improvements**
|
||||
|
||||
- Centralized validation logic in the `OCROptions` Pydantic model
|
||||
- Centralized validation logic in the `OcrOptions` Pydantic model
|
||||
- Removed scattered option mutation throughout the codebase
|
||||
- Better type safety for plugin development
|
||||
- Simplified plugin option handling
|
||||
|
||||
**Migration guide for plugin developers**
|
||||
|
||||
- Update imports: `from ocrmypdf._options import OCROptions`
|
||||
- Update type hints: `def check_options(options: OCROptions)` instead of `options: Namespace`
|
||||
- Update imports: `from ocrmypdf._options import OcrOptions`
|
||||
- Update type hints: `def check_options(options: OcrOptions)` instead of `options: Namespace`
|
||||
- Attribute access remains unchanged: `options.languages`, `options.output_type`, etc.
|
||||
- Remove any in-place option modifications - compute values at point of use instead
|
||||
- Most existing plugins will continue working without changes due to duck-typing
|
||||
|
||||
+16
-12
@@ -9,7 +9,7 @@ from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
from ocrmypdf.pdfinfo.info import PageInfo
|
||||
|
||||
@@ -20,14 +20,16 @@ if TYPE_CHECKING:
|
||||
class PdfContext:
|
||||
"""Holds the context for a particular run of the pipeline."""
|
||||
|
||||
options: OCROptions #: The specified options for processing this PDF.
|
||||
options: OcrOptions #: The specified options for processing this PDF.
|
||||
origin: Path #: The filename of the original input file.
|
||||
pdfinfo: PdfInfo #: Detailed data for this PDF.
|
||||
plugin_manager: OcrmypdfPluginManager #: PluginManager for processing the current PDF.
|
||||
plugin_manager: (
|
||||
OcrmypdfPluginManager #: PluginManager for processing the current PDF.
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
work_folder: Path,
|
||||
origin: Path,
|
||||
pdfinfo: PdfInfo,
|
||||
@@ -66,23 +68,25 @@ class PageContext:
|
||||
Must be pickle-able, so stores only intrinsic/simple data elements or those
|
||||
capable of their serializing themselves via ``__getstate__``.
|
||||
|
||||
Note: Uses OCROptions with JSON serialization for multiprocessing compatibility.
|
||||
Note: Uses OcrOptions with JSON serialization for multiprocessing compatibility.
|
||||
"""
|
||||
|
||||
origin: Path #: The filename of the original input file.
|
||||
pageno: int #: This page number (zero-based).
|
||||
pageinfo: PageInfo #: Information on this page.
|
||||
plugin_manager: OcrmypdfPluginManager #: PluginManager for processing the current PDF.
|
||||
plugin_manager: (
|
||||
OcrmypdfPluginManager #: PluginManager for processing the current PDF.
|
||||
)
|
||||
|
||||
def __init__(self, pdf_context: PdfContext, pageno):
|
||||
self.work_folder = pdf_context.work_folder
|
||||
self.origin = pdf_context.origin
|
||||
# Store OCROptions directly instead of Namespace
|
||||
# Store OcrOptions directly instead of Namespace
|
||||
self.options = pdf_context.options
|
||||
self.pageno = pageno
|
||||
self.pageinfo = pdf_context.pdfinfo[pageno]
|
||||
self.plugin_manager = pdf_context.plugin_manager
|
||||
# Ensure no reference to PdfContext which contains OCROptions
|
||||
# Ensure no reference to PdfContext which contains OcrOptions
|
||||
self._pdf_context = None
|
||||
|
||||
def get_path(self, name: str) -> Path:
|
||||
@@ -98,7 +102,7 @@ class PageContext:
|
||||
|
||||
options_json = self.options.model_dump_json_safe()
|
||||
state['options_json'] = options_json
|
||||
# Remove the OCROptions object to avoid pickle issues
|
||||
# Remove the OcrOptions object to avoid pickle issues
|
||||
del state['options']
|
||||
|
||||
# Remove any potential references to Pydantic objects
|
||||
@@ -108,10 +112,10 @@ class PageContext:
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
|
||||
# Reconstruct OCROptions from JSON if available
|
||||
# Reconstruct OcrOptions from JSON if available
|
||||
if 'options_json' in state:
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
|
||||
self.options = OCROptions.model_validate_json_safe(state['options_json'])
|
||||
self.options = OcrOptions.model_validate_json_safe(state['options_json'])
|
||||
# Otherwise, we have a fallback Namespace (shouldn't happen in normal operation)
|
||||
# Leave it as-is for compatibility
|
||||
|
||||
@@ -90,7 +90,7 @@ def _pages_from_ranges(ranges: str) -> set[int]:
|
||||
return set(pages)
|
||||
|
||||
|
||||
class OCROptions(BaseModel):
|
||||
class OcrOptions(BaseModel):
|
||||
"""Internal options model that can masquerade as argparse.Namespace.
|
||||
|
||||
This model provides proper typing and validation while maintaining
|
||||
@@ -210,7 +210,6 @@ class OCROptions(BaseModel):
|
||||
default_factory=dict, exclude=True, alias='_extra_attrs'
|
||||
)
|
||||
|
||||
|
||||
@field_validator('languages')
|
||||
@classmethod
|
||||
def validate_languages(cls, v):
|
||||
@@ -459,7 +458,7 @@ class OCROptions(BaseModel):
|
||||
return json.dumps(serializable_data)
|
||||
|
||||
@classmethod
|
||||
def model_validate_json_safe(cls, json_str: str) -> OCROptions:
|
||||
def model_validate_json_safe(cls, json_str: str) -> OcrOptions:
|
||||
"""Reconstruct from JSON with special handling for non-serializable types."""
|
||||
data = json.loads(json_str)
|
||||
|
||||
@@ -547,25 +546,25 @@ class OCROptions(BaseModel):
|
||||
for field_name in model_class.model_fields:
|
||||
# Try namespace_field pattern first (e.g., tesseract_timeout)
|
||||
flat_name = f"{namespace}_{field_name}"
|
||||
if flat_name in OCROptions.model_fields:
|
||||
if flat_name in OcrOptions.model_fields:
|
||||
value = getattr(self, flat_name)
|
||||
if value is not None:
|
||||
kwargs[field_name] = _convert_value(value)
|
||||
# Also check direct field name (for fields like jbig2_lossy)
|
||||
elif field_name in OCROptions.model_fields:
|
||||
elif field_name in OcrOptions.model_fields:
|
||||
value = getattr(self, field_name)
|
||||
if value is not None:
|
||||
kwargs[field_name] = _convert_value(value)
|
||||
# Check for special mappings
|
||||
elif namespace == 'optimize' and field_name == 'level':
|
||||
# 'optimize' field maps to 'level' in OptimizeOptions
|
||||
if 'optimize' in OCROptions.model_fields:
|
||||
if 'optimize' in OcrOptions.model_fields:
|
||||
value = getattr(self, 'optimize')
|
||||
if value is not None:
|
||||
kwargs[field_name] = _convert_value(value)
|
||||
elif namespace == 'optimize' and field_name == 'jpeg_quality':
|
||||
# jpg_quality maps to jpeg_quality
|
||||
if 'jpg_quality' in OCROptions.model_fields:
|
||||
if 'jpg_quality' in OcrOptions.model_fields:
|
||||
value = getattr(self, 'jpg_quality')
|
||||
if value is not None:
|
||||
kwargs[field_name] = _convert_value(value)
|
||||
|
||||
@@ -28,7 +28,7 @@ from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf._exec import unpaper
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext
|
||||
from ocrmypdf._metadata import repair_docinfo_nuls
|
||||
from ocrmypdf._options import OCROptions, ProcessingMode
|
||||
from ocrmypdf._options import OcrOptions, ProcessingMode
|
||||
from ocrmypdf.exceptions import (
|
||||
DigitalSignatureError,
|
||||
DpiError,
|
||||
@@ -64,7 +64,7 @@ VECTOR_PAGE_DPI = 400
|
||||
register_heif_opener()
|
||||
|
||||
|
||||
def triage_image_file(input_file: Path, output_file: Path, options: OCROptions) -> None:
|
||||
def triage_image_file(input_file: Path, output_file: Path, options: OcrOptions) -> None:
|
||||
"""Triage the input image file.
|
||||
|
||||
If the input file is an image, check its resolution and convert it to PDF.
|
||||
@@ -163,7 +163,7 @@ def _pdf_guess_version(input_file: Path, search_window=1024) -> str:
|
||||
|
||||
|
||||
def triage(
|
||||
original_filename: str, input_file: Path, output_file: Path, options: OCROptions
|
||||
original_filename: str, input_file: Path, output_file: Path, options: OcrOptions
|
||||
) -> Path:
|
||||
"""Triage the input file. We can handle PDFs and images."""
|
||||
try:
|
||||
|
||||
@@ -30,7 +30,7 @@ from ocrmypdf._concurrent import Executor, setup_executor
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext
|
||||
from ocrmypdf._logging import PageNumberFilter
|
||||
from ocrmypdf._metadata import metadata_fixup
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._pipeline import (
|
||||
convert_to_pdfa,
|
||||
create_ocr_image,
|
||||
@@ -205,7 +205,7 @@ def worker_init(max_pixels: int | None) -> None:
|
||||
@contextmanager
|
||||
def manage_debug_log_handler(
|
||||
*,
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
work_folder: Path,
|
||||
):
|
||||
remover = None
|
||||
@@ -254,8 +254,8 @@ def manage_work_folder(*, work_folder: Path, retain: bool, print_location: bool)
|
||||
|
||||
|
||||
def cli_exception_handler(
|
||||
fn: Callable[[OCROptions, OcrmypdfPluginManager], ExitCode],
|
||||
options: OCROptions,
|
||||
fn: Callable[[OcrOptions, OcrmypdfPluginManager], ExitCode],
|
||||
options: OcrOptions,
|
||||
plugin_manager: OcrmypdfPluginManager,
|
||||
) -> ExitCode:
|
||||
"""Convert exceptions into command line error messages and exit codes.
|
||||
@@ -319,14 +319,14 @@ def cli_exception_handler(
|
||||
|
||||
|
||||
def setup_pipeline(
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
plugin_manager: OcrmypdfPluginManager,
|
||||
) -> Executor:
|
||||
# Any changes to options will not take effect for options that are already
|
||||
# bound to function parameters in the pipeline. (For example
|
||||
# options.input_file, options.pdf_renderer are already bound.)
|
||||
# Note: OCROptions is immutable, so we can't modify options.jobs directly
|
||||
# The jobs field should already be set correctly during OCROptions creation
|
||||
# Note: OcrOptions is immutable, so we can't modify options.jobs directly
|
||||
# The jobs field should already be set correctly during OcrOptions creation
|
||||
|
||||
# Apply PIL max image pixels side effect
|
||||
PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000)
|
||||
|
||||
@@ -16,7 +16,7 @@ import PIL
|
||||
from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf._graft import OcrGrafter
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._pipeline import copy_final
|
||||
from ocrmypdf._pipelines._common import (
|
||||
HOCRResult,
|
||||
@@ -104,7 +104,7 @@ def exec_hocr_to_ocr_pdf(context: PdfContext, executor: Executor) -> Sequence[st
|
||||
|
||||
|
||||
def run_hocr_to_ocr_pdf_pipeline(
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
*,
|
||||
plugin_manager: OcrmypdfPluginManager,
|
||||
) -> ExitCode:
|
||||
|
||||
@@ -18,7 +18,7 @@ import PIL
|
||||
from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf._graft import OcrGrafter
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._pipeline import (
|
||||
copy_final,
|
||||
is_ocr_required,
|
||||
@@ -162,7 +162,7 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
||||
|
||||
|
||||
def _run_pipeline(
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
plugin_manager: OcrmypdfPluginManager,
|
||||
) -> ExitCode:
|
||||
with (
|
||||
@@ -197,7 +197,7 @@ def _run_pipeline(
|
||||
|
||||
|
||||
def run_pipeline_cli(
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
*,
|
||||
plugin_manager: OcrmypdfPluginManager,
|
||||
) -> ExitCode:
|
||||
@@ -212,7 +212,7 @@ def run_pipeline_cli(
|
||||
|
||||
|
||||
def run_pipeline(
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
*,
|
||||
plugin_manager: OcrmypdfPluginManager,
|
||||
) -> ExitCode:
|
||||
|
||||
@@ -15,7 +15,7 @@ import PIL
|
||||
|
||||
from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._pipeline import (
|
||||
is_ocr_required,
|
||||
ocr_engine_hocr,
|
||||
@@ -84,7 +84,7 @@ def exec_pdf_to_hocr(context: PdfContext, executor: Executor) -> None:
|
||||
|
||||
|
||||
def run_hocr_pipeline(
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
*,
|
||||
plugin_manager: OcrmypdfPluginManager,
|
||||
) -> None:
|
||||
|
||||
@@ -20,7 +20,7 @@ from pydantic import BaseModel
|
||||
|
||||
import ocrmypdf.builtin_plugins
|
||||
from ocrmypdf import Executor, PdfContext, pluginspec
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._progressbar import ProgressBar
|
||||
from ocrmypdf.helpers import Resolution
|
||||
from ocrmypdf.pluginspec import OcrEngine
|
||||
@@ -140,7 +140,7 @@ class OcrmypdfPluginManager:
|
||||
rotation: int | None,
|
||||
filter_vector: bool,
|
||||
stop_on_soft_error: bool,
|
||||
options: OCROptions | None,
|
||||
options: OcrOptions | None,
|
||||
use_cropbox: bool,
|
||||
) -> Path | None:
|
||||
"""Rasterize one page of a PDF at specified resolution."""
|
||||
@@ -178,11 +178,11 @@ class OcrmypdfPluginManager:
|
||||
page=page, image_filename=image_filename, output_pdf=output_pdf
|
||||
)
|
||||
|
||||
def get_ocr_engine(self, *, options: OCROptions | None = None) -> OcrEngine | None:
|
||||
def get_ocr_engine(self, *, options: OcrOptions | None = None) -> OcrEngine | None:
|
||||
"""Returns an OcrEngine to use for processing.
|
||||
|
||||
Args:
|
||||
options: OCROptions to pass to the hook for engine selection.
|
||||
options: OcrOptions to pass to the hook for engine selection.
|
||||
"""
|
||||
return self._pm.hook.get_ocr_engine(options=options)
|
||||
|
||||
@@ -251,11 +251,11 @@ class OcrmypdfPluginManager:
|
||||
"""Returns plugin option models keyed by namespace."""
|
||||
return self._pm.hook.register_options()
|
||||
|
||||
def check_options(self, *, options: OCROptions) -> list[None]:
|
||||
def check_options(self, *, options: OcrOptions) -> list[None]:
|
||||
"""Called to validate options after parsing."""
|
||||
return self._pm.hook.check_options(options=options)
|
||||
|
||||
def validate(self, *, pdfinfo: PdfInfo, options: OCROptions) -> list[None]:
|
||||
def validate(self, *, pdfinfo: PdfInfo, options: OcrOptions) -> list[None]:
|
||||
"""Called to validate options and pdfinfo after PDF is loaded."""
|
||||
return self._pm.hook.validate(pdfinfo=pdfinfo, options=options)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class PluginOptionRegistry:
|
||||
"""Registry for plugin option models.
|
||||
|
||||
This registry collects option models from plugins during initialization.
|
||||
Plugin options can be accessed via nested namespaces on OCROptions
|
||||
Plugin options can be accessed via nested namespaces on OcrOptions
|
||||
(e.g., options.tesseract.timeout) or via flat field names for backward
|
||||
compatibility (e.g., options.tesseract_timeout).
|
||||
"""
|
||||
|
||||
+11
-10
@@ -17,7 +17,7 @@ import pikepdf
|
||||
|
||||
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
|
||||
from ocrmypdf._exec import unpaper
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._plugin_manager import OcrmypdfPluginManager
|
||||
from ocrmypdf.exceptions import (
|
||||
BadArgsError,
|
||||
@@ -47,7 +47,7 @@ def check_platform() -> None:
|
||||
|
||||
|
||||
def check_options_languages(
|
||||
options: OCROptions, ocr_engine_languages: list[str]
|
||||
options: OcrOptions, ocr_engine_languages: list[str]
|
||||
) -> None:
|
||||
if not ocr_engine_languages:
|
||||
return
|
||||
@@ -72,7 +72,7 @@ def check_options_languages(
|
||||
raise MissingDependencyError(msg)
|
||||
|
||||
|
||||
def check_options_sidecar(options: OCROptions) -> None:
|
||||
def check_options_sidecar(options: OcrOptions) -> None:
|
||||
if options.sidecar == '\0':
|
||||
if options.output_file == '-':
|
||||
raise BadArgsError("--sidecar filename needed when output file is stdout.")
|
||||
@@ -87,7 +87,7 @@ def check_options_sidecar(options: OCROptions) -> None:
|
||||
)
|
||||
|
||||
|
||||
def check_options_preprocessing(options: OCROptions) -> None:
|
||||
def check_options_preprocessing(options: OcrOptions) -> None:
|
||||
if options.clean_final:
|
||||
options.clean = True
|
||||
if options.unpaper_args and not options.clean:
|
||||
@@ -114,14 +114,14 @@ def check_options_preprocessing(options: OCROptions) -> None:
|
||||
raise BadArgsError("--unpaper-args: " + str(e)) from e
|
||||
|
||||
|
||||
def _check_plugin_invariant_options(options: OCROptions) -> None:
|
||||
def _check_plugin_invariant_options(options: OcrOptions) -> None:
|
||||
check_platform()
|
||||
check_options_sidecar(options)
|
||||
check_options_preprocessing(options)
|
||||
|
||||
|
||||
def _check_plugin_options(
|
||||
options: OCROptions, plugin_manager: OcrmypdfPluginManager
|
||||
options: OcrOptions, plugin_manager: OcrmypdfPluginManager
|
||||
) -> None:
|
||||
# First, let plugins check their external dependencies
|
||||
plugin_manager.check_options(options=options)
|
||||
@@ -134,11 +134,12 @@ def _check_plugin_options(
|
||||
|
||||
# Finally, run comprehensive validation using the coordinator
|
||||
from ocrmypdf._validation_coordinator import ValidationCoordinator
|
||||
|
||||
coordinator = ValidationCoordinator(plugin_manager)
|
||||
coordinator.validate_all_options(options)
|
||||
|
||||
|
||||
def check_options(options: OCROptions, plugin_manager: OcrmypdfPluginManager) -> None:
|
||||
def check_options(options: OcrOptions, plugin_manager: OcrmypdfPluginManager) -> None:
|
||||
"""Check options for validity and consistency.
|
||||
|
||||
This function coordinates validation across the entire system:
|
||||
@@ -151,7 +152,7 @@ def check_options(options: OCROptions, plugin_manager: OcrmypdfPluginManager) ->
|
||||
_check_plugin_options(options, plugin_manager)
|
||||
|
||||
|
||||
def create_input_file(options: OCROptions, work_folder: Path) -> tuple[Path, str]:
|
||||
def create_input_file(options: OcrOptions, work_folder: Path) -> tuple[Path, str]:
|
||||
if options.input_file == '-':
|
||||
# stdin
|
||||
log.info('reading file from standard input')
|
||||
@@ -196,7 +197,7 @@ def create_input_file(options: OCROptions, work_folder: Path) -> tuple[Path, str
|
||||
raise InputFileError(msg) from e
|
||||
|
||||
|
||||
def check_requested_output_file(options: OCROptions) -> None:
|
||||
def check_requested_output_file(options: OcrOptions) -> None:
|
||||
if options.output_file == '-':
|
||||
if sys.stdout.isatty():
|
||||
raise BadArgsError(
|
||||
@@ -214,7 +215,7 @@ def check_requested_output_file(options: OCROptions) -> None:
|
||||
|
||||
|
||||
def report_output_file_size(
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
input_file: Path,
|
||||
output_file: Path,
|
||||
optimize_messages: Sequence[str] | None = None,
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
import pluggy
|
||||
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,7 +24,7 @@ class ValidationCoordinator:
|
||||
self.plugin_manager = plugin_manager
|
||||
self.registry = getattr(plugin_manager, '_option_registry', None)
|
||||
|
||||
def validate_all_options(self, options: OCROptions) -> None:
|
||||
def validate_all_options(self, options: OcrOptions) -> None:
|
||||
"""Run comprehensive validation on all options.
|
||||
|
||||
This runs validation in the correct order:
|
||||
@@ -41,7 +41,7 @@ class ValidationCoordinator:
|
||||
# Step 2: Cross-cutting validation
|
||||
self._validate_cross_cutting_concerns(options)
|
||||
|
||||
def _validate_plugin_contexts(self, options: OCROptions) -> None:
|
||||
def _validate_plugin_contexts(self, options: OcrOptions) -> None:
|
||||
"""Validate plugin options that require external context."""
|
||||
# For now, we'll run the plugin validation directly since the models
|
||||
# are still being integrated. This ensures the validation warnings
|
||||
@@ -53,7 +53,7 @@ class ValidationCoordinator:
|
||||
# Run Optimize validation
|
||||
self._validate_optimize_options(options)
|
||||
|
||||
def _validate_tesseract_options(self, options: OCROptions) -> None:
|
||||
def _validate_tesseract_options(self, options: OcrOptions) -> None:
|
||||
"""Validate Tesseract options."""
|
||||
# Check pagesegmode warning
|
||||
if options.tesseract.pagesegmode in (0, 2):
|
||||
@@ -74,6 +74,7 @@ class ValidationCoordinator:
|
||||
|
||||
# Check for blocked languages
|
||||
from ocrmypdf.exceptions import BadArgsError
|
||||
|
||||
DENIED_LANGUAGES = {'equ', 'osd'}
|
||||
if DENIED_LANGUAGES & set(options.languages):
|
||||
raise BadArgsError(
|
||||
@@ -83,19 +84,21 @@ class ValidationCoordinator:
|
||||
"Remove them from the -l/--language argument."
|
||||
)
|
||||
|
||||
def _validate_optimize_options(self, options: OCROptions) -> None:
|
||||
def _validate_optimize_options(self, options: OcrOptions) -> None:
|
||||
"""Validate optimization options."""
|
||||
# Check optimization consistency
|
||||
if options.optimize == 0 and any([
|
||||
options.png_quality and options.png_quality > 0,
|
||||
options.jpeg_quality and options.jpeg_quality > 0
|
||||
]):
|
||||
if options.optimize == 0 and any(
|
||||
[
|
||||
options.png_quality and options.png_quality > 0,
|
||||
options.jpeg_quality and options.jpeg_quality > 0,
|
||||
]
|
||||
):
|
||||
log.warning(
|
||||
"The arguments --png-quality and --jpeg-quality "
|
||||
"will be ignored because --optimize=0."
|
||||
)
|
||||
|
||||
def _validate_cross_cutting_concerns(self, options: OCROptions) -> None:
|
||||
def _validate_cross_cutting_concerns(self, options: OcrOptions) -> None:
|
||||
"""Validate cross-cutting concerns that span multiple plugins."""
|
||||
from ocrmypdf._options import ProcessingMode
|
||||
|
||||
@@ -115,7 +118,8 @@ class ValidationCoordinator:
|
||||
|
||||
# Validate output type compatibility
|
||||
if options.output_type == 'none' and str(options.output_file) not in (
|
||||
os.devnull, '-'
|
||||
os.devnull,
|
||||
'-',
|
||||
):
|
||||
raise ValueError(
|
||||
"Since you specified `--output-type none`, the output file "
|
||||
@@ -134,7 +138,7 @@ class ValidationCoordinator:
|
||||
"--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
|
||||
)
|
||||
|
||||
def _handle_deprecated_pdf_renderer(self, options: OCROptions) -> None:
|
||||
def _handle_deprecated_pdf_renderer(self, options: OcrOptions) -> None:
|
||||
"""Handle deprecated pdf_renderer values by redirecting to fpdf2."""
|
||||
if options.pdf_renderer in ('hocr', 'hocrdebug'):
|
||||
log.info(
|
||||
|
||||
+30
-30
@@ -51,7 +51,7 @@ from typing import BinaryIO
|
||||
from warnings import warn
|
||||
|
||||
from ocrmypdf._logging import PageNumberFilter
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._pipelines.hocr_to_ocr_pdf import run_hocr_to_ocr_pdf_pipeline
|
||||
from ocrmypdf._pipelines.ocr import run_pipeline, run_pipeline_cli
|
||||
from ocrmypdf._pipelines.pdf_to_hocr import run_hocr_pipeline
|
||||
@@ -120,8 +120,8 @@ def setup_plugin_infrastructure(
|
||||
registry.register_option_model(namespace, model_class)
|
||||
all_plugin_models[namespace] = model_class
|
||||
|
||||
# Register plugin models with OCROptions for dynamic nested access
|
||||
OCROptions.register_plugin_models(all_plugin_models)
|
||||
# Register plugin models with OcrOptions for dynamic nested access
|
||||
OcrOptions.register_plugin_models(all_plugin_models)
|
||||
|
||||
# Store registry in plugin manager for later access
|
||||
plugin_manager._option_registry = registry
|
||||
@@ -234,7 +234,7 @@ def configure_logging(
|
||||
|
||||
def create_options(
|
||||
*, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs
|
||||
) -> OCROptions:
|
||||
) -> OcrOptions:
|
||||
"""Construct an options object from the input/output files and keyword arguments.
|
||||
|
||||
Args:
|
||||
@@ -244,12 +244,12 @@ def create_options(
|
||||
**kwargs: Keyword arguments.
|
||||
|
||||
Returns:
|
||||
OCROptions: An options object containing the parsed arguments.
|
||||
OcrOptions: An options object containing the parsed arguments.
|
||||
|
||||
Raises:
|
||||
TypeError: If the type of a keyword argument is not supported.
|
||||
"""
|
||||
# Prepare kwargs for direct OCROptions construction
|
||||
# Prepare kwargs for direct OcrOptions construction
|
||||
options_kwargs = kwargs.copy()
|
||||
|
||||
# Set input and output files
|
||||
@@ -260,16 +260,16 @@ def create_options(
|
||||
if 'sidecar' in options_kwargs and isinstance(
|
||||
options_kwargs['sidecar'], BinaryIO | IOBase
|
||||
):
|
||||
# Keep the stream object as-is - OCROptions can handle it
|
||||
# Keep the stream object as-is - OcrOptions can handle it
|
||||
pass
|
||||
|
||||
# Remove None values to let OCROptions use its defaults
|
||||
# Remove None values to let OcrOptions use its defaults
|
||||
options_kwargs = {k: v for k, v in options_kwargs.items() if v is not None}
|
||||
|
||||
# Remove any kwargs that aren't OCROptions fields and store in extra_attrs
|
||||
# Remove any kwargs that aren't OcrOptions fields and store in extra_attrs
|
||||
extra_attrs = {}
|
||||
ocr_fields = set(OCROptions.model_fields.keys())
|
||||
# Legacy mode flags are handled by OCROptions model validator
|
||||
ocr_fields = set(OcrOptions.model_fields.keys())
|
||||
# Legacy mode flags are handled by OcrOptions model validator
|
||||
legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'}
|
||||
|
||||
# Known extra attributes that should be preserved
|
||||
@@ -280,16 +280,16 @@ def create_options(
|
||||
continue
|
||||
extra_attrs[key] = options_kwargs.pop(key)
|
||||
|
||||
# Create OCROptions directly
|
||||
# Create OcrOptions directly
|
||||
try:
|
||||
options = OCROptions(**options_kwargs)
|
||||
options = OcrOptions(**options_kwargs)
|
||||
# Add any extra attributes
|
||||
if extra_attrs:
|
||||
options.extra_attrs.update(extra_attrs)
|
||||
return options
|
||||
except Exception as e:
|
||||
# If direct construction fails, provide a helpful error message
|
||||
raise TypeError(f"Failed to create OCROptions: {e}") from e
|
||||
raise TypeError(f"Failed to create OcrOptions: {e}") from e
|
||||
|
||||
|
||||
def ocr( # noqa: D417
|
||||
@@ -538,7 +538,7 @@ def _pdf_to_hocr( # noqa: D417
|
||||
else:
|
||||
plugins = list(plugins)
|
||||
|
||||
# Prepare kwargs for direct OCROptions construction
|
||||
# Prepare kwargs for direct OcrOptions construction
|
||||
options_kwargs = kwargs.copy()
|
||||
|
||||
# Set input file and handle special output_folder case
|
||||
@@ -558,16 +558,16 @@ def _pdf_to_hocr( # noqa: D417
|
||||
if plugins:
|
||||
options_kwargs['plugins'] = plugins
|
||||
|
||||
# Remove None values to let OCROptions use its defaults
|
||||
# Remove None values to let OcrOptions use its defaults
|
||||
options_kwargs = {k: v for k, v in options_kwargs.items() if v is not None}
|
||||
|
||||
# Add output_folder to options_kwargs since it's now a proper field
|
||||
options_kwargs['output_folder'] = output_folder
|
||||
|
||||
# Remove any kwargs that aren't OCROptions fields and store in extra_attrs
|
||||
# Remove any kwargs that aren't OcrOptions fields and store in extra_attrs
|
||||
extra_attrs = {}
|
||||
ocr_fields = set(OCROptions.model_fields.keys())
|
||||
# Legacy mode flags are handled by OCROptions model validator
|
||||
ocr_fields = set(OcrOptions.model_fields.keys())
|
||||
# Legacy mode flags are handled by OcrOptions model validator
|
||||
legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'}
|
||||
known_extra = {'progress_bar', 'plugins'}
|
||||
|
||||
@@ -584,15 +584,15 @@ def _pdf_to_hocr( # noqa: D417
|
||||
|
||||
plugin_manager.add_options(parser=get_parser())
|
||||
|
||||
# Create OCROptions directly
|
||||
# Create OcrOptions directly
|
||||
try:
|
||||
options = OCROptions(**options_kwargs)
|
||||
options = OcrOptions(**options_kwargs)
|
||||
# Add any extra attributes
|
||||
if extra_attrs:
|
||||
options.extra_attrs.update(extra_attrs)
|
||||
except Exception as e:
|
||||
raise TypeError(
|
||||
f"Failed to create OCROptions for hOCR pipeline: {e}"
|
||||
f"Failed to create OcrOptions for hOCR pipeline: {e}"
|
||||
) from e
|
||||
|
||||
return run_hocr_pipeline(options=options, plugin_manager=plugin_manager)
|
||||
@@ -643,7 +643,7 @@ def _hocr_to_ocr_pdf( # noqa: D417
|
||||
else:
|
||||
plugins = list(plugins)
|
||||
|
||||
# Prepare kwargs for direct OCROptions construction
|
||||
# Prepare kwargs for direct OcrOptions construction
|
||||
options_kwargs = kwargs.copy()
|
||||
|
||||
# Set output file and handle special work_folder case
|
||||
@@ -663,7 +663,7 @@ def _hocr_to_ocr_pdf( # noqa: D417
|
||||
if plugins:
|
||||
options_kwargs['plugins'] = plugins
|
||||
|
||||
# Remove None values to let OCROptions use its defaults
|
||||
# Remove None values to let OcrOptions use its defaults
|
||||
options_kwargs = {k: v for k, v in options_kwargs.items() if v is not None}
|
||||
|
||||
# Warn about deprecated jbig2 options and remove from kwargs
|
||||
@@ -680,10 +680,10 @@ def _hocr_to_ocr_pdf( # noqa: D417
|
||||
# Add work_folder to options_kwargs since it's now a proper field
|
||||
options_kwargs['work_folder'] = work_folder
|
||||
|
||||
# Remove any kwargs that aren't OCROptions fields and store in extra_attrs
|
||||
# Remove any kwargs that aren't OcrOptions fields and store in extra_attrs
|
||||
extra_attrs = {}
|
||||
ocr_fields = set(OCROptions.model_fields.keys())
|
||||
# Legacy mode flags are handled by OCROptions model validator
|
||||
ocr_fields = set(OcrOptions.model_fields.keys())
|
||||
# Legacy mode flags are handled by OcrOptions model validator
|
||||
legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'}
|
||||
known_extra = {'progress_bar', 'plugins'}
|
||||
|
||||
@@ -700,15 +700,15 @@ def _hocr_to_ocr_pdf( # noqa: D417
|
||||
|
||||
plugin_manager.add_options(parser=get_parser())
|
||||
|
||||
# Create OCROptions directly
|
||||
# Create OcrOptions directly
|
||||
try:
|
||||
options = OCROptions(**options_kwargs)
|
||||
options = OcrOptions(**options_kwargs)
|
||||
# Add any extra attributes
|
||||
if extra_attrs:
|
||||
options.extra_attrs.update(extra_attrs)
|
||||
except Exception as e:
|
||||
raise TypeError(
|
||||
f"Failed to create OCROptions for hOCR to PDF pipeline: {e}"
|
||||
f"Failed to create OcrOptions for hOCR to PDF pipeline: {e}"
|
||||
) from e
|
||||
|
||||
return run_hocr_to_ocr_pdf_pipeline(
|
||||
|
||||
@@ -23,7 +23,7 @@ from ocrmypdf.hocrtransform import BoundingBox, OcrClass, OcrElement
|
||||
from ocrmypdf.pluginspec import OcrEngine, OrientationConfidence
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
|
||||
|
||||
class NullOcrEngine(OcrEngine):
|
||||
@@ -39,7 +39,7 @@ class NullOcrEngine(OcrEngine):
|
||||
return "none"
|
||||
|
||||
@staticmethod
|
||||
def creator_tag(options: OCROptions) -> str:
|
||||
def creator_tag(options: OcrOptions) -> str:
|
||||
"""Return creator tag for PDF metadata."""
|
||||
return "OCRmyPDF (no OCR)"
|
||||
|
||||
@@ -48,17 +48,17 @@ class NullOcrEngine(OcrEngine):
|
||||
return "No OCR engine"
|
||||
|
||||
@staticmethod
|
||||
def languages(options: OCROptions) -> set[str]:
|
||||
def languages(options: OcrOptions) -> set[str]:
|
||||
"""Return supported languages (empty set for null engine)."""
|
||||
return set()
|
||||
|
||||
@staticmethod
|
||||
def get_orientation(input_file: Path, options: OCROptions) -> OrientationConfidence:
|
||||
def get_orientation(input_file: Path, options: OcrOptions) -> OrientationConfidence:
|
||||
"""Return neutral orientation (no rotation detected)."""
|
||||
return OrientationConfidence(angle=0, confidence=0.0)
|
||||
|
||||
@staticmethod
|
||||
def get_deskew(input_file: Path, options: OCROptions) -> float:
|
||||
def get_deskew(input_file: Path, options: OcrOptions) -> float:
|
||||
"""Return zero deskew angle."""
|
||||
return 0.0
|
||||
|
||||
@@ -70,7 +70,7 @@ class NullOcrEngine(OcrEngine):
|
||||
@staticmethod
|
||||
def generate_ocr(
|
||||
input_file: Path,
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
page_number: int = 0,
|
||||
) -> tuple[OcrElement, str]:
|
||||
"""Generate empty OCR results.
|
||||
@@ -104,7 +104,7 @@ class NullOcrEngine(OcrEngine):
|
||||
input_file: Path,
|
||||
output_hocr: Path,
|
||||
output_text: Path,
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
) -> None:
|
||||
"""Generate empty hOCR file.
|
||||
|
||||
@@ -137,7 +137,7 @@ class NullOcrEngine(OcrEngine):
|
||||
input_file: Path,
|
||||
output_pdf: Path,
|
||||
output_text: Path,
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
) -> None:
|
||||
"""NullOcrEngine cannot generate PDFs directly.
|
||||
|
||||
|
||||
+11
-11
@@ -12,7 +12,7 @@ from typing import Any, TypeVar
|
||||
|
||||
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
|
||||
from ocrmypdf._defaults import PROGRAM_NAME as _PROGRAM_NAME
|
||||
from ocrmypdf._options import OCROptions, ProcessingMode
|
||||
from ocrmypdf._options import OcrOptions, ProcessingMode
|
||||
from ocrmypdf._plugin_manager import OcrmypdfPluginManager
|
||||
from ocrmypdf._version import __version__ as _VERSION
|
||||
|
||||
@@ -473,8 +473,8 @@ plugins_only_parser.add_argument(
|
||||
)
|
||||
|
||||
|
||||
def namespace_to_options(ns) -> OCROptions:
|
||||
"""Convert argparse.Namespace to OCROptions.
|
||||
def namespace_to_options(ns) -> OcrOptions:
|
||||
"""Convert argparse.Namespace to OcrOptions.
|
||||
|
||||
This function encapsulates CLI-specific knowledge of how command line
|
||||
arguments map to our internal options model.
|
||||
@@ -483,14 +483,14 @@ def namespace_to_options(ns) -> OCROptions:
|
||||
known_fields = {}
|
||||
extra_attrs = {}
|
||||
|
||||
# Legacy boolean flags that map to mode - handled by OCROptions model validator
|
||||
# Legacy boolean flags that map to mode - handled by OcrOptions model validator
|
||||
legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'}
|
||||
|
||||
for key, value in vars(ns).items():
|
||||
if key in OCROptions.model_fields:
|
||||
if key in OcrOptions.model_fields:
|
||||
known_fields[key] = value
|
||||
elif key in legacy_mode_flags:
|
||||
# Pass legacy flags to OCROptions for conversion to mode
|
||||
# Pass legacy flags to OcrOptions for conversion to mode
|
||||
known_fields[key] = value
|
||||
else:
|
||||
extra_attrs[key] = value
|
||||
@@ -503,15 +503,15 @@ 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
|
||||
|
||||
instance = OCROptions(**known_fields)
|
||||
instance = OcrOptions(**known_fields)
|
||||
instance.extra_attrs = extra_attrs
|
||||
return instance
|
||||
|
||||
|
||||
def get_options_and_plugins(
|
||||
args=None,
|
||||
) -> tuple[OCROptions, OcrmypdfPluginManager]:
|
||||
"""Parse command line arguments and return OCROptions and plugin manager.
|
||||
) -> tuple[OcrOptions, OcrmypdfPluginManager]:
|
||||
"""Parse command line arguments and return OcrOptions and plugin manager.
|
||||
|
||||
This is the main entry point for CLI argument processing. It handles
|
||||
plugin discovery, argument parsing, and conversion to our internal
|
||||
@@ -521,7 +521,7 @@ def get_options_and_plugins(
|
||||
args: Command line arguments. If None, uses sys.argv.
|
||||
|
||||
Returns:
|
||||
Tuple of (OCROptions, PluginManager)
|
||||
Tuple of (OcrOptions, PluginManager)
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from ocrmypdf.api import setup_plugin_infrastructure
|
||||
@@ -539,7 +539,7 @@ def get_options_and_plugins(
|
||||
# Parse all arguments
|
||||
namespace = parser.parse_args(args=args)
|
||||
|
||||
# Convert to OCROptions
|
||||
# Convert to OcrOptions
|
||||
options = namespace_to_options(namespace)
|
||||
|
||||
return options, plugin_manager
|
||||
|
||||
@@ -721,12 +721,12 @@ def main(infile, outfile, level, jobs=1):
|
||||
from shutil import copy # pylint: disable=import-outside-toplevel
|
||||
from tempfile import TemporaryDirectory # pylint: disable=import-outside-toplevel
|
||||
|
||||
from ocrmypdf._options import OCROptions # pylint: disable=import-outside-toplevel
|
||||
from ocrmypdf._options import OcrOptions # pylint: disable=import-outside-toplevel
|
||||
|
||||
infile = Path(infile)
|
||||
|
||||
# Create OCROptions with optimization-specific settings
|
||||
options = OCROptions(
|
||||
# Create OcrOptions with optimization-specific settings
|
||||
options = OcrOptions(
|
||||
input_file=infile,
|
||||
output_file=outfile, # Required field
|
||||
jobs=jobs,
|
||||
|
||||
+13
-13
@@ -16,7 +16,7 @@ import pluggy
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ocrmypdf import Executor, PdfContext
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._progressbar import ProgressBar
|
||||
from ocrmypdf.helpers import Resolution
|
||||
|
||||
@@ -112,7 +112,7 @@ def register_options() -> dict[str, type[BaseModel]]:
|
||||
|
||||
|
||||
@hookspec
|
||||
def check_options(options: OCROptions) -> None:
|
||||
def check_options(options: OcrOptions) -> None:
|
||||
"""Called to ask the plugin to check all of the options.
|
||||
|
||||
The plugin may check if options that it added are valid.
|
||||
@@ -183,7 +183,7 @@ def get_progressbar_class() -> type[ProgressBar]: # type: ignore[return-value]
|
||||
|
||||
|
||||
@hookspec
|
||||
def validate(pdfinfo: PdfInfo, options: OCROptions) -> None:
|
||||
def validate(pdfinfo: PdfInfo, options: OcrOptions) -> None:
|
||||
"""Called to give a plugin an opportunity to review *options* and *pdfinfo*.
|
||||
|
||||
*options* contains the "work order" to process a particular file. *pdfinfo*
|
||||
@@ -214,7 +214,7 @@ def rasterize_pdf_page(
|
||||
rotation: int | None,
|
||||
filter_vector: bool,
|
||||
stop_on_soft_error: bool,
|
||||
options: OCROptions | None,
|
||||
options: OcrOptions | None,
|
||||
use_cropbox: bool,
|
||||
) -> Path: # type: ignore[return-value]
|
||||
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
|
||||
@@ -401,7 +401,7 @@ class OcrEngine(ABC):
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def creator_tag(options: OCROptions) -> str:
|
||||
def creator_tag(options: OcrOptions) -> str:
|
||||
"""Returns the creator tag to identify this software's role in creating the PDF.
|
||||
|
||||
This tag will be inserted in the XMP metadata and DocumentInfo dictionary
|
||||
@@ -422,7 +422,7 @@ class OcrEngine(ABC):
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def languages(options: OCROptions) -> Set[str]:
|
||||
def languages(options: OcrOptions) -> Set[str]:
|
||||
"""Returns the set of all languages that are supported by the engine.
|
||||
|
||||
Languages are typically given in 3-letter ISO 3166-1 codes, but actually
|
||||
@@ -431,18 +431,18 @@ class OcrEngine(ABC):
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_orientation(input_file: Path, options: OCROptions) -> OrientationConfidence:
|
||||
def get_orientation(input_file: Path, options: OcrOptions) -> OrientationConfidence:
|
||||
"""Returns the orientation of the image."""
|
||||
|
||||
@staticmethod
|
||||
def get_deskew(input_file: Path, options: OCROptions) -> float:
|
||||
def get_deskew(input_file: Path, options: OcrOptions) -> float:
|
||||
"""Returns the deskew angle of the image, in degrees."""
|
||||
return 0.0
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def generate_hocr(
|
||||
input_file: Path, output_hocr: Path, output_text: Path, options: OCROptions
|
||||
input_file: Path, output_hocr: Path, output_text: Path, options: OcrOptions
|
||||
) -> None:
|
||||
"""Called to produce a hOCR file from a page image and sidecar text file.
|
||||
|
||||
@@ -465,7 +465,7 @@ class OcrEngine(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def generate_pdf(
|
||||
input_file: Path, output_pdf: Path, output_text: Path, options: OCROptions
|
||||
input_file: Path, output_pdf: Path, output_text: Path, options: OcrOptions
|
||||
) -> None:
|
||||
"""Called to produce a text only PDF from a page image.
|
||||
|
||||
@@ -501,7 +501,7 @@ class OcrEngine(ABC):
|
||||
@staticmethod
|
||||
def generate_ocr(
|
||||
input_file: Path,
|
||||
options: OCROptions,
|
||||
options: OcrOptions,
|
||||
page_number: int = 0,
|
||||
) -> tuple[OcrElement, str]:
|
||||
"""Generate OCR results as an OcrElement tree.
|
||||
@@ -531,7 +531,7 @@ class OcrEngine(ABC):
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def get_ocr_engine(options: OCROptions | None) -> OcrEngine: # type: ignore[return-value]
|
||||
def get_ocr_engine(options: OcrOptions | None) -> OcrEngine: # type: ignore[return-value]
|
||||
"""Returns an OcrEngine to use for processing this file.
|
||||
|
||||
The OcrEngine may be instantiated multiple times, by both the main process
|
||||
@@ -542,7 +542,7 @@ def get_ocr_engine(options: OCROptions | None) -> OcrEngine: # type: ignore[ret
|
||||
engine. The hook caller will then try the next plugin.
|
||||
|
||||
Args:
|
||||
options: The current OCROptions, used to determine which engine
|
||||
options: The current OcrOptions, used to determine which engine
|
||||
to select. May be None for backward compatibility with external
|
||||
plugins.
|
||||
|
||||
|
||||
+2
-2
@@ -107,14 +107,14 @@ def test_hocr_result_pickle():
|
||||
|
||||
def test_nested_plugin_option_access():
|
||||
"""Test that plugin options can be accessed via nested namespaces."""
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf.api import setup_plugin_infrastructure
|
||||
|
||||
# Set up plugin infrastructure to register plugin models
|
||||
setup_plugin_infrastructure()
|
||||
|
||||
# Create options with tesseract settings
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file='test.pdf',
|
||||
output_file='output.pdf',
|
||||
tesseract_timeout=120.0,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test JSON serialization of OCROptions for multiprocessing compatibility."""
|
||||
"""Test JSON serialization of OcrOptions for multiprocessing compatibility."""
|
||||
|
||||
import multiprocessing
|
||||
from io import BytesIO
|
||||
@@ -6,28 +6,28 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOptions
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_plugin_models():
|
||||
"""Register plugin models for tests."""
|
||||
OCROptions.register_plugin_models({'tesseract': TesseractOptions})
|
||||
OcrOptions.register_plugin_models({'tesseract': TesseractOptions})
|
||||
yield
|
||||
# Clean up after test (optional, but good practice)
|
||||
|
||||
|
||||
def worker_function(options_json: str) -> str:
|
||||
"""Worker function that deserializes OCROptions from JSON and returns a result."""
|
||||
"""Worker function that deserializes OcrOptions from JSON and returns a result."""
|
||||
# Register plugin models in worker process
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOptions
|
||||
|
||||
OCROptions.register_plugin_models({'tesseract': TesseractOptions})
|
||||
OcrOptions.register_plugin_models({'tesseract': TesseractOptions})
|
||||
|
||||
# Reconstruct OCROptions from JSON in worker process
|
||||
options = OCROptions.model_validate_json_safe(options_json)
|
||||
# Reconstruct OcrOptions from JSON in worker process
|
||||
options = OcrOptions.model_validate_json_safe(options_json)
|
||||
|
||||
# Verify we can access various option types
|
||||
# Count only user-added extra_attrs (exclude plugin cache keys starting with '_')
|
||||
@@ -51,9 +51,9 @@ def worker_function(options_json: str) -> str:
|
||||
|
||||
|
||||
def test_json_serialization_multiprocessing():
|
||||
"""Test that OCROptions can be JSON serialized and used in multiprocessing."""
|
||||
# Create OCROptions with various field types
|
||||
options = OCROptions(
|
||||
"""Test that OcrOptions can be JSON serialized and used in multiprocessing."""
|
||||
# Create OcrOptions with various field types
|
||||
options = OcrOptions(
|
||||
input_file=Path('/test/input.pdf'),
|
||||
output_file=Path('/test/output.pdf'),
|
||||
languages=['eng', 'deu'],
|
||||
@@ -72,7 +72,7 @@ def test_json_serialization_multiprocessing():
|
||||
options_json = options.model_dump_json_safe()
|
||||
|
||||
# Test that we can deserialize in the main process
|
||||
reconstructed = OCROptions.model_validate_json_safe(options_json)
|
||||
reconstructed = OcrOptions.model_validate_json_safe(options_json)
|
||||
assert reconstructed.input_file == options.input_file
|
||||
assert reconstructed.output_file == options.output_file
|
||||
assert reconstructed.languages == options.languages
|
||||
@@ -112,7 +112,7 @@ def test_json_serialization_with_streams():
|
||||
input_stream = BytesIO(b'fake pdf data')
|
||||
output_stream = BytesIO()
|
||||
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file=input_stream,
|
||||
output_file=output_stream,
|
||||
languages=['eng'],
|
||||
@@ -123,7 +123,7 @@ def test_json_serialization_with_streams():
|
||||
options_json = options.model_dump_json_safe()
|
||||
|
||||
# Deserialize (streams will be placeholder strings)
|
||||
reconstructed = OCROptions.model_validate_json_safe(options_json)
|
||||
reconstructed = OcrOptions.model_validate_json_safe(options_json)
|
||||
|
||||
# Streams should be converted to placeholder strings
|
||||
assert reconstructed.input_file == 'stream'
|
||||
@@ -134,7 +134,7 @@ def test_json_serialization_with_streams():
|
||||
|
||||
def test_json_serialization_with_none_values():
|
||||
"""Test JSON serialization handles None values correctly."""
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file=Path('/test/input.pdf'),
|
||||
output_file=Path('/test/output.pdf'),
|
||||
languages=['eng'],
|
||||
@@ -145,7 +145,7 @@ def test_json_serialization_with_none_values():
|
||||
options_json = options.model_dump_json_safe()
|
||||
|
||||
# Deserialize
|
||||
reconstructed = OCROptions.model_validate_json_safe(options_json)
|
||||
reconstructed = OcrOptions.model_validate_json_safe(options_json)
|
||||
|
||||
# Verify None values are preserved (check actual defaults from model)
|
||||
assert reconstructed.tesseract_timeout == 0.0 # Default value, not None
|
||||
|
||||
@@ -74,14 +74,14 @@ class TestOcrEngineCliOption:
|
||||
|
||||
|
||||
class TestOcrEngineOptionsModel:
|
||||
"""Test OCROptions has ocr_engine field."""
|
||||
"""Test OcrOptions has ocr_engine field."""
|
||||
|
||||
def test_ocr_options_has_ocr_engine_field(self):
|
||||
"""OCROptions should have ocr_engine field."""
|
||||
from ocrmypdf._options import OCROptions
|
||||
"""OcrOptions should have ocr_engine field."""
|
||||
from ocrmypdf._options import OcrOptions
|
||||
|
||||
# Check field exists in model
|
||||
assert 'ocr_engine' in OCROptions.model_fields
|
||||
assert 'ocr_engine' in OcrOptions.model_fields
|
||||
|
||||
|
||||
class TestOcrEnginePluginSelection:
|
||||
@@ -91,8 +91,8 @@ class TestOcrEnginePluginSelection:
|
||||
"""TesseractOcrEngine should be returned when ocr_engine='auto'."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine
|
||||
from ocrmypdf.builtin_plugins import tesseract_ocr
|
||||
from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine
|
||||
|
||||
options = MagicMock()
|
||||
options.ocr_engine = 'auto'
|
||||
@@ -104,8 +104,8 @@ class TestOcrEnginePluginSelection:
|
||||
"""TesseractOcrEngine should be returned when ocr_engine='tesseract'."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine
|
||||
from ocrmypdf.builtin_plugins import tesseract_ocr
|
||||
from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine
|
||||
|
||||
options = MagicMock()
|
||||
options.ocr_engine = 'tesseract'
|
||||
@@ -117,8 +117,8 @@ class TestOcrEnginePluginSelection:
|
||||
"""NullOcrEngine should be returned when ocr_engine='none'."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ocrmypdf.builtin_plugins.null_ocr import NullOcrEngine
|
||||
from ocrmypdf.builtin_plugins import null_ocr
|
||||
from ocrmypdf.builtin_plugins.null_ocr import NullOcrEngine
|
||||
|
||||
options = MagicMock()
|
||||
options.ocr_engine = 'none'
|
||||
|
||||
+16
-17
@@ -12,7 +12,7 @@ import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution
|
||||
|
||||
@@ -67,7 +67,7 @@ class TestRasterizerOption:
|
||||
def test_rasterizer_invalid(self):
|
||||
"""Test that an invalid rasterizer value is rejected."""
|
||||
with pytest.raises(ValueError, match="rasterizer must be one of"):
|
||||
OCROptions(
|
||||
OcrOptions(
|
||||
input_file='test.pdf', output_file='out.pdf', rasterizer='invalid'
|
||||
)
|
||||
|
||||
@@ -127,7 +127,7 @@ class TestRasterizerHookDirect:
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
# Create options requesting pypdfium
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='pypdfium',
|
||||
@@ -162,7 +162,7 @@ class TestRasterizerHookDirect:
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
# Create options requesting ghostscript
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='ghostscript',
|
||||
@@ -190,7 +190,7 @@ class TestRasterizerHookDirect:
|
||||
"""Test that auto mode uses pypdfium when available."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='auto',
|
||||
@@ -363,7 +363,7 @@ class TestRasterizerWithNonStandardBoxes:
|
||||
"""Compare output dimensions between rasterizers for nonstandard boxes."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options_gs = OCROptions(
|
||||
options_gs = OcrOptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out_gs.pdf',
|
||||
rasterizer='ghostscript',
|
||||
@@ -388,7 +388,7 @@ class TestRasterizerWithNonStandardBoxes:
|
||||
gs_size = im_gs.size
|
||||
|
||||
if PYPDFIUM_AVAILABLE:
|
||||
options_pdfium = OCROptions(
|
||||
options_pdfium = OcrOptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out_pdfium.pdf',
|
||||
rasterizer='pypdfium',
|
||||
@@ -445,7 +445,7 @@ class TestRasterizerWithRotationAndBoxes:
|
||||
"""Test Ghostscript produces correct dimensions with rotation."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='ghostscript',
|
||||
@@ -481,13 +481,11 @@ class TestRasterizerWithRotationAndBoxes:
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not PYPDFIUM_AVAILABLE, reason="pypdfium2 not installed")
|
||||
def test_pypdfium_rotation_dimensions(
|
||||
self, pdf_with_nonstandard_boxes, tmp_path
|
||||
):
|
||||
def test_pypdfium_rotation_dimensions(self, pdf_with_nonstandard_boxes, tmp_path):
|
||||
"""Test pypdfium produces correct dimensions with rotation."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='pypdfium',
|
||||
@@ -535,7 +533,7 @@ class TestRasterizerWithRotationAndBoxes:
|
||||
|
||||
for rotation in [0, 90, 180, 270]:
|
||||
# Rasterize with Ghostscript
|
||||
gs_options = OCROptions(
|
||||
gs_options = OcrOptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='ghostscript',
|
||||
@@ -556,7 +554,7 @@ class TestRasterizerWithRotationAndBoxes:
|
||||
)
|
||||
|
||||
# Rasterize with pypdfium
|
||||
pdfium_options = OCROptions(
|
||||
pdfium_options = OcrOptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='pypdfium',
|
||||
@@ -577,9 +575,10 @@ class TestRasterizerWithRotationAndBoxes:
|
||||
)
|
||||
|
||||
# Verify both produce the same MediaBox dimensions
|
||||
with Image.open(gs_img_path) as gs_img, Image.open(
|
||||
pdfium_img_path
|
||||
) as pdfium_img:
|
||||
with (
|
||||
Image.open(gs_img_path) as gs_img,
|
||||
Image.open(pdfium_img_path) as pdfium_img,
|
||||
):
|
||||
expected = self._get_expected_size(rotation)
|
||||
|
||||
assert abs(gs_img.size[0] - expected[0]) <= 2, (
|
||||
|
||||
@@ -329,11 +329,11 @@ def test_rotate_and_crop(
|
||||
|
||||
|
||||
def test_rasterize_rotates(resources, tmp_path):
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options = OCROptions(
|
||||
options = OcrOptions(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='ghostscript', # Use Ghostscript for consistent dimensions
|
||||
|
||||
@@ -13,7 +13,7 @@ import pytest
|
||||
from ocrmypdf import _validation as vd
|
||||
from ocrmypdf._concurrent import NullProgressBar, SerialExecutor
|
||||
from ocrmypdf._exec.tesseract import TesseractVersion
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._options import OcrOptions
|
||||
from ocrmypdf.api import create_options, setup_plugin_infrastructure
|
||||
from ocrmypdf.cli import get_parser
|
||||
from ocrmypdf.exceptions import BadArgsError, MissingDependencyError
|
||||
@@ -42,8 +42,8 @@ def make_opts(*args, **kwargs):
|
||||
|
||||
|
||||
def make_ocr_opts(input_file='a.pdf', output_file='b.pdf', **kwargs):
|
||||
"""Create OCROptions directly for testing Pydantic validation."""
|
||||
return OCROptions(input_file=input_file, output_file=output_file, **kwargs)
|
||||
"""Create OcrOptions directly for testing Pydantic validation."""
|
||||
return OcrOptions(input_file=input_file, output_file=output_file, **kwargs)
|
||||
|
||||
|
||||
def test_old_tesseract_error():
|
||||
@@ -95,7 +95,7 @@ def test_optimizing(caplog):
|
||||
|
||||
|
||||
def test_pillow_options():
|
||||
# Test that max_image_mpixels=0 is valid (validation now in OCROptions)
|
||||
# Test that max_image_mpixels=0 is valid (validation now in OcrOptions)
|
||||
opts = make_ocr_opts(max_image_mpixels=0)
|
||||
assert opts.max_image_mpixels == 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user