Reduces mypy errors in src/ocrmypdf and tests from 89 to 51. - Replace the `deprecation` package with stdlib `warnings.deprecated` (falling back to typing_extensions on <3.13); drop the dependency. - Add pypdfium2/uharfbuzz/pi_heif to mypy's ignore_missing_imports overrides (no upstream stubs); drop pluggy, which now ships py.typed. - Add a tests.* mypy override so test functions aren't required to annotate -> None. Real bugs found and fixed along the way, not just annotations: - OcrmypdfPluginManager had a `pluggy` property shadowing the `pluggy` module import within its own class body, breaking every `pluggy.PluginManager` annotation below it; renamed to `pluggy_manager`. - `_option_registry` was bolted onto OcrmypdfPluginManager from outside and read via `getattr(..., None)` instead of being a declared attribute; declared it properly. - ValidationCoordinator.__init__ was typed to accept a raw pluggy.PluginManager, but every caller passes the OcrmypdfPluginManager wrapper. - check_options_sidecar() did `options.output_file + '.txt'`, assuming output_file is always a str; would raise a raw TypeError if ever hit with a stream/bytes output. Added an explicit guard. - is_file_writable() called Path(test_file), which raises TypeError on a bytes path; fixed via os.fsdecode(). - copy_final() had a dead, unused `original_file` parameter; removed it. - run_hocr_pipeline() constructed PdfContext with the raw, untriaged input_file instead of the locally-copied origin_pdf, inconsistent with the other two pipelines. - _options.py had jbig2_threshold declared twice in the same model.
149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
# SPDX-FileCopyrightText: 2024 James R. Barlow
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
|
|
"""Validation coordinator for plugin options and cross-cutting concerns."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from ocrmypdf._options import OcrOptions
|
|
from ocrmypdf._plugin_manager import OcrmypdfPluginManager
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class ValidationCoordinator:
|
|
"""Coordinates validation across plugin models and core options."""
|
|
|
|
def __init__(self, plugin_manager: OcrmypdfPluginManager):
|
|
self.plugin_manager = plugin_manager
|
|
self.registry = plugin_manager._option_registry
|
|
|
|
def validate_all_options(self, options: OcrOptions) -> None:
|
|
"""Run comprehensive validation on all options.
|
|
|
|
This runs validation in the correct order:
|
|
1. Plugin self-validation (already done by Pydantic)
|
|
2. Plugin context validation (requires external context)
|
|
3. Cross-cutting validation (between plugins and core)
|
|
|
|
Args:
|
|
options: The options to validate
|
|
"""
|
|
# Step 1: Plugin context validation
|
|
self._validate_plugin_contexts(options)
|
|
|
|
# Step 2: Cross-cutting validation
|
|
self._validate_cross_cutting_concerns(options)
|
|
|
|
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
|
|
# and checks still work as expected.
|
|
|
|
# Run Tesseract validation
|
|
self._validate_tesseract_options(options)
|
|
|
|
# Run Optimize validation
|
|
self._validate_optimize_options(options)
|
|
|
|
def _validate_tesseract_options(self, options: OcrOptions) -> None:
|
|
"""Validate Tesseract options."""
|
|
# Check pagesegmode warning
|
|
if options.tesseract.pagesegmode in (0, 2):
|
|
log.warning(
|
|
"The tesseract-pagesegmode you selected will disable OCR. "
|
|
"This may cause processing to fail."
|
|
)
|
|
|
|
# Check downsample consistency
|
|
if (
|
|
options.tesseract.downsample_above != 32767
|
|
and not options.tesseract.downsample_large_images
|
|
):
|
|
log.warning(
|
|
"The --tesseract-downsample-above argument will have no effect unless "
|
|
"--tesseract-downsample-large-images is also given."
|
|
)
|
|
|
|
# Note: blocked languages (equ, osd) are checked earlier in
|
|
# check_options_languages() to ensure the check runs before
|
|
# the missing language check.
|
|
|
|
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,
|
|
]
|
|
):
|
|
log.warning(
|
|
"The arguments --png-quality and --jpeg-quality "
|
|
"will be ignored because --optimize=0."
|
|
)
|
|
|
|
def _validate_cross_cutting_concerns(self, options: OcrOptions) -> None:
|
|
"""Validate cross-cutting concerns that span multiple plugins."""
|
|
from ocrmypdf._options import ProcessingMode
|
|
|
|
# Handle deprecated pdf_renderer values
|
|
self._handle_deprecated_pdf_renderer(options)
|
|
|
|
# Note: Mutual exclusivity of force_ocr/skip_text/redo_ocr is now enforced
|
|
# by the ProcessingMode enum - only one mode can be active at a time.
|
|
|
|
# Validate redo mode compatibility
|
|
if options.mode == ProcessingMode.redo and (
|
|
options.deskew or options.clean_final or options.remove_background
|
|
):
|
|
raise ValueError(
|
|
"--redo-ocr (or --mode redo) is not currently compatible with "
|
|
"--deskew, --clean-final, and --remove-background"
|
|
)
|
|
|
|
# Validate output type compatibility
|
|
output_file_display = (
|
|
os.fsdecode(options.output_file)
|
|
if isinstance(options.output_file, bytes)
|
|
else str(options.output_file)
|
|
)
|
|
if options.output_type == 'none' and output_file_display not in (
|
|
os.devnull,
|
|
'-',
|
|
):
|
|
raise ValueError(
|
|
"Since you specified `--output-type none`, the output file "
|
|
f"{output_file_display} cannot be produced. Set the output file to "
|
|
"`-` to suppress this message."
|
|
)
|
|
|
|
# Validate PDF/A image compression compatibility
|
|
if (
|
|
options.ghostscript.pdfa_image_compression
|
|
and options.ghostscript.pdfa_image_compression != 'auto'
|
|
and not options.output_type.startswith('pdfa')
|
|
):
|
|
log.warning(
|
|
"--pdfa-image-compression argument only applies when "
|
|
"--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
|
|
)
|
|
|
|
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(
|
|
"The '%s' PDF renderer has been removed. Using 'fpdf2' instead, "
|
|
"which provides full international language support, proper RTL "
|
|
"rendering, and improved text positioning.",
|
|
options.pdf_renderer,
|
|
)
|
|
# Modify the options object to use fpdf2
|
|
object.__setattr__(options, 'pdf_renderer', 'fpdf2')
|