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:
James R. Barlow
2026-01-12 23:37:54 -08:00
parent 36dea181e6
commit 740f67091c
26 changed files with 197 additions and 190 deletions
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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.
:::
+5 -5
View File
@@ -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
View File
@@ -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
+6 -7
View File
@@ -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)
+3 -3
View File
@@ -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:
+7 -7
View File
@@ -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)
+2 -2
View File
@@ -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:
+4 -4
View File
@@ -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:
+2 -2
View File
@@ -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:
+6 -6
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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,
+15 -11
View File
@@ -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([
if options.optimize == 0 and any(
[
options.png_quality and options.png_quality > 0,
options.jpeg_quality and options.jpeg_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
View File
@@ -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(
+8 -8
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+16 -16
View File
@@ -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
+7 -7
View File
@@ -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
View File
@@ -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, (
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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