Add --mode/-m CLI argument with ProcessingMode enum
Introduce a new --mode (-m) argument that consolidates the three mutually exclusive OCR processing options into a single enum: - default: Error if text is found (standard behavior) - force: Rasterize all content and run OCR (replaces --force-ocr) - skip: Skip pages with existing text (replaces --skip-text) - redo: Re-OCR pages, stripping old text layer (replaces --redo-ocr) The legacy flags --force-ocr, --skip-text, and --redo-ocr remain as silent aliases for backward compatibility. Both CLI and API usage continue to work unchanged.
This commit is contained in:
@@ -27,6 +27,7 @@ from pikepdf import (
|
||||
)
|
||||
|
||||
from ocrmypdf._jobcontext import PdfContext
|
||||
from ocrmypdf._options import ProcessingMode
|
||||
from ocrmypdf._pipeline import VECTOR_PAGE_DPI
|
||||
|
||||
|
||||
@@ -492,8 +493,8 @@ class OcrGrafter:
|
||||
|
||||
new_text_layer = Stream(self.pdf_base, pdf_draw_xobj)
|
||||
|
||||
# Strip old invisible text if redo_ocr is enabled
|
||||
if self.context.options.redo_ocr:
|
||||
# Strip old invisible text if redo mode is enabled
|
||||
if self.context.options.mode == ProcessingMode.redo:
|
||||
strip_invisible_text(self.pdf_base, base_page)
|
||||
|
||||
# Add text layer to base page
|
||||
@@ -585,7 +586,7 @@ class OcrGrafter:
|
||||
pdf_draw_xobj = b'q\n' + (b'%s Do\n' % text_xobj_name) + b'\nQ\n'
|
||||
new_text_layer = Stream(self.pdf_base, pdf_draw_xobj)
|
||||
|
||||
if self.context.options.redo_ocr:
|
||||
if self.context.options.mode == ProcessingMode.redo:
|
||||
strip_invisible_text(self.pdf_base, base_page)
|
||||
base_page.contents_coalesce()
|
||||
base_page.contents_add(
|
||||
|
||||
+78
-19
@@ -10,6 +10,7 @@ import logging
|
||||
import os
|
||||
import unicodedata
|
||||
from collections.abc import Sequence
|
||||
from enum import StrEnum
|
||||
from io import IOBase
|
||||
from pathlib import Path
|
||||
from typing import Any, BinaryIO
|
||||
@@ -32,6 +33,23 @@ _plugin_option_models: dict[str, type] = {}
|
||||
PathOrIO = BinaryIO | IOBase | Path | str | bytes
|
||||
|
||||
|
||||
class ProcessingMode(StrEnum):
|
||||
"""OCR processing mode for handling pages with existing text.
|
||||
|
||||
This enum controls how OCRmyPDF handles pages that already contain text:
|
||||
|
||||
- ``default``: Error if text is found (standard OCR behavior)
|
||||
- ``force``: Rasterize all content and run OCR regardless of existing text
|
||||
- ``skip``: Skip OCR on pages that already have text
|
||||
- ``redo``: Re-OCR pages, stripping old invisible text layer
|
||||
"""
|
||||
|
||||
default = 'default'
|
||||
force = 'force'
|
||||
skip = 'skip'
|
||||
redo = 'redo'
|
||||
|
||||
|
||||
def _pages_from_ranges(ranges: str) -> set[int]:
|
||||
"""Convert page range string to set of page numbers."""
|
||||
pages: list[int] = []
|
||||
@@ -89,9 +107,23 @@ class OCROptions(BaseModel):
|
||||
# Core OCR options
|
||||
languages: list[str] = Field(default_factory=lambda: [DEFAULT_LANGUAGE])
|
||||
output_type: str = 'auto'
|
||||
force_ocr: bool = False
|
||||
skip_text: bool = False
|
||||
redo_ocr: bool = False
|
||||
mode: ProcessingMode = ProcessingMode.default
|
||||
|
||||
# Backward compatibility properties for force_ocr, skip_text, redo_ocr
|
||||
@property
|
||||
def force_ocr(self) -> bool:
|
||||
"""Backward compatibility alias for mode == ProcessingMode.force."""
|
||||
return self.mode == ProcessingMode.force
|
||||
|
||||
@property
|
||||
def skip_text(self) -> bool:
|
||||
"""Backward compatibility alias for mode == ProcessingMode.skip."""
|
||||
return self.mode == ProcessingMode.skip
|
||||
|
||||
@property
|
||||
def redo_ocr(self) -> bool:
|
||||
"""Backward compatibility alias for mode == ProcessingMode.redo."""
|
||||
return self.mode == ProcessingMode.redo
|
||||
|
||||
# Job control
|
||||
jobs: int | None = None
|
||||
@@ -296,31 +328,58 @@ class OCROptions(BaseModel):
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def handle_special_cases(cls, data):
|
||||
"""Handle special cases for API compatibility."""
|
||||
"""Handle special cases for API compatibility and legacy options."""
|
||||
if isinstance(data, dict):
|
||||
# For hOCR API, output_file might not be present
|
||||
if 'output_folder' in data and 'output_file' not in data:
|
||||
data['output_file'] = '/dev/null' # Placeholder
|
||||
|
||||
# Convert legacy boolean options (force_ocr, skip_text, redo_ocr) to mode
|
||||
force = data.pop('force_ocr', None)
|
||||
skip = data.pop('skip_text', None)
|
||||
redo = data.pop('redo_ocr', None)
|
||||
|
||||
# Count how many legacy options are set to True
|
||||
legacy_set = [
|
||||
(force, ProcessingMode.force),
|
||||
(skip, ProcessingMode.skip),
|
||||
(redo, ProcessingMode.redo),
|
||||
]
|
||||
legacy_true = [(val, mode) for val, mode in legacy_set if val]
|
||||
legacy_count = len(legacy_true)
|
||||
|
||||
# Get current mode value (may be string or enum)
|
||||
current_mode = data.get('mode', ProcessingMode.default)
|
||||
if isinstance(current_mode, str):
|
||||
current_mode = ProcessingMode(current_mode)
|
||||
mode_is_set = current_mode != ProcessingMode.default
|
||||
|
||||
if legacy_count > 1:
|
||||
raise ValueError(
|
||||
"Choose only one of --force-ocr, --skip-text, --redo-ocr."
|
||||
)
|
||||
|
||||
if legacy_count == 1:
|
||||
expected_mode = legacy_true[0][1]
|
||||
if mode_is_set and current_mode != expected_mode:
|
||||
legacy_flag = f"--{expected_mode.value.replace('_', '-')}-ocr"
|
||||
raise ValueError(
|
||||
f"Conflicting options: --mode {current_mode.value} "
|
||||
f"cannot be used with {legacy_flag} or similar legacy flag."
|
||||
)
|
||||
# Set mode from legacy option
|
||||
data['mode'] = expected_mode
|
||||
|
||||
return data
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_exclusive_ocr_options(self):
|
||||
"""Ensure only one of force_ocr, skip_text, redo_ocr is set."""
|
||||
exclusive_options = sum(
|
||||
1 for opt in [self.force_ocr, self.skip_text, self.redo_ocr] if opt
|
||||
)
|
||||
if exclusive_options >= 2:
|
||||
raise ValueError("Choose only one of --force-ocr, --skip-text, --redo-ocr.")
|
||||
return self
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_redo_ocr_options(self):
|
||||
"""Validate options compatible with redo_ocr."""
|
||||
if self.redo_ocr:
|
||||
"""Validate options compatible with redo mode."""
|
||||
if self.mode == ProcessingMode.redo:
|
||||
if self.deskew or self.clean_final or self.remove_background:
|
||||
raise ValueError(
|
||||
"--redo-ocr is not currently compatible with --deskew, "
|
||||
"--clean-final, and --remove-background"
|
||||
"--redo-ocr (or --mode redo) is not currently compatible with "
|
||||
"--deskew, --clean-final, and --remove-background"
|
||||
)
|
||||
return self
|
||||
|
||||
@@ -345,7 +404,7 @@ class OCROptions(BaseModel):
|
||||
[
|
||||
self.deskew,
|
||||
self.clean_final,
|
||||
self.force_ocr,
|
||||
self.mode == ProcessingMode.force,
|
||||
self.remove_background,
|
||||
]
|
||||
)
|
||||
|
||||
+25
-25
@@ -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
|
||||
from ocrmypdf._options import OCROptions, ProcessingMode
|
||||
from ocrmypdf.exceptions import (
|
||||
DigitalSignatureError,
|
||||
DpiError,
|
||||
@@ -233,10 +233,10 @@ def validate_pdfinfo_options(context: PdfContext) -> None:
|
||||
else:
|
||||
raise DigitalSignatureError()
|
||||
if pdfinfo.has_acroform:
|
||||
if options.redo_ocr:
|
||||
if options.mode == ProcessingMode.redo:
|
||||
raise InputFileError(
|
||||
"This PDF has a user fillable form. --redo-ocr is not "
|
||||
"currently possible on such files."
|
||||
"This PDF has a user fillable form. --redo-ocr (or --mode redo) "
|
||||
"is not currently possible on such files."
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
@@ -244,14 +244,14 @@ def validate_pdfinfo_options(context: PdfContext) -> None:
|
||||
"Chances are it is a pure digital "
|
||||
"document that does not need OCR."
|
||||
)
|
||||
if not options.force_ocr:
|
||||
if options.mode != ProcessingMode.force:
|
||||
log.info(
|
||||
"Use the option --force-ocr to produce an image of the "
|
||||
"form and all filled form fields. The output PDF will be "
|
||||
"'flattened' and will no longer be fillable."
|
||||
"Use the option --force-ocr (or --mode force) to produce an "
|
||||
"image of the form and all filled form fields. The output PDF "
|
||||
"will be 'flattened' and will no longer be fillable."
|
||||
)
|
||||
if pdfinfo.is_tagged:
|
||||
if options.force_ocr or options.skip_text or options.redo_ocr:
|
||||
if options.mode != ProcessingMode.default:
|
||||
log.warning(
|
||||
"This PDF is marked as a Tagged PDF. This often indicates "
|
||||
"that the PDF was generated from an office document and does "
|
||||
@@ -328,24 +328,24 @@ def is_ocr_required(page_context: PageContext) -> bool:
|
||||
log.debug(f"skipped {pageinfo.pageno} as requested by --pages {options.pages}")
|
||||
ocr_required = False
|
||||
elif pageinfo.has_text:
|
||||
if not options.force_ocr and not (options.skip_text or options.redo_ocr):
|
||||
if options.mode == ProcessingMode.default:
|
||||
raise PriorOcrFoundError(
|
||||
"page already has text! - aborting (use --force-ocr to force OCR; "
|
||||
" see also help for the arguments --skip-text and --redo-ocr"
|
||||
"page already has text! - aborting (use --force-ocr or --mode force "
|
||||
"to force OCR; see also help for --skip-text, --redo-ocr, and --mode)"
|
||||
)
|
||||
elif options.force_ocr:
|
||||
elif options.mode == ProcessingMode.force:
|
||||
log.info("page already has text! - rasterizing text and running OCR anyway")
|
||||
ocr_required = True
|
||||
elif options.redo_ocr:
|
||||
elif options.mode == ProcessingMode.redo:
|
||||
if pageinfo.has_corrupt_text:
|
||||
log.warning(
|
||||
"some text on this page cannot be mapped to characters: "
|
||||
"consider using --force-ocr instead"
|
||||
"consider using --force-ocr (or --mode force) instead"
|
||||
)
|
||||
else:
|
||||
log.info("redoing OCR")
|
||||
ocr_required = True
|
||||
elif options.skip_text:
|
||||
elif options.mode == ProcessingMode.skip:
|
||||
log.info("skipping all processing on this page")
|
||||
ocr_required = False
|
||||
elif not pageinfo.images and not options.lossless_reconstruction:
|
||||
@@ -356,14 +356,14 @@ def is_ocr_required(page_context: PageContext) -> bool:
|
||||
# ahead and rasterize. If not forced, then pretend there's no text
|
||||
# on the page at all so we don't lose anything.
|
||||
# This could be made smarter by explicitly searching for vector art.
|
||||
if options.force_ocr and options.oversample:
|
||||
if options.mode == ProcessingMode.force and options.oversample:
|
||||
# The user really wants to reprocess this file
|
||||
log.info(
|
||||
"page has no images - "
|
||||
f"rasterizing at {options.oversample} DPI because "
|
||||
"--force-ocr --oversample was specified"
|
||||
"--force-ocr --oversample (or --mode force --oversample) was specified"
|
||||
)
|
||||
elif options.force_ocr:
|
||||
elif options.mode == ProcessingMode.force:
|
||||
# Warn the user they might not want to do this
|
||||
log.warning(
|
||||
"page has no images - "
|
||||
@@ -376,8 +376,8 @@ def is_ocr_required(page_context: PageContext) -> bool:
|
||||
log.info(
|
||||
"page has no images - "
|
||||
"skipping all processing on this page to avoid losing detail. "
|
||||
"Use --force-ocr if you wish to perform OCR on pages that "
|
||||
"have vector content."
|
||||
"Use --force-ocr (or --mode force) if you wish to perform OCR on "
|
||||
"pages that have vector content."
|
||||
)
|
||||
ocr_required = False
|
||||
|
||||
@@ -645,11 +645,11 @@ def create_ocr_image(image: Path, page_context: PageContext) -> Path:
|
||||
with Image.open(image) as im:
|
||||
log.debug('resolution %r', im.info['dpi'])
|
||||
|
||||
if not options.force_ocr:
|
||||
if options.mode != ProcessingMode.force:
|
||||
# Do not mask text areas when forcing OCR, because we need to OCR
|
||||
# all text areas
|
||||
mask = None # Exclude both visible and invisible text from OCR
|
||||
if options.redo_ocr:
|
||||
if options.mode == ProcessingMode.redo:
|
||||
mask = True # Mask visible text, but not invisible text
|
||||
|
||||
draw = ImageDraw.ImageDraw(im)
|
||||
@@ -1092,8 +1092,8 @@ def _is_safe_pdfa(input_pdf: Path, options) -> bool:
|
||||
if pdfa_status['pass']:
|
||||
return True
|
||||
|
||||
# Safe if we rewrote the PDF with force-ocr
|
||||
if options.force_ocr:
|
||||
# Safe if we rewrote the PDF with force mode
|
||||
if options.mode == ProcessingMode.force:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -243,13 +243,15 @@ def report_output_file_size(
|
||||
'clean_final',
|
||||
'remove_background',
|
||||
'oversample',
|
||||
'force_ocr',
|
||||
}
|
||||
for arg in image_preproc:
|
||||
if getattr(options, arg, False):
|
||||
reasons.append(
|
||||
f"--{arg.replace('_', '-')} was issued, causing transcoding."
|
||||
)
|
||||
# Check force_ocr via the backward-compatible property
|
||||
if options.force_ocr:
|
||||
reasons.append("--force-ocr (or --mode force) was issued, causing transcoding.")
|
||||
|
||||
reasons.extend(optimize_messages)
|
||||
|
||||
|
||||
@@ -97,22 +97,20 @@ class ValidationCoordinator:
|
||||
|
||||
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)
|
||||
|
||||
# Validate mutually exclusive OCR options
|
||||
exclusive_options = sum(
|
||||
1 for opt in [options.force_ocr, options.skip_text, options.redo_ocr] if opt
|
||||
)
|
||||
if exclusive_options >= 2:
|
||||
raise ValueError("Choose only one of --force-ocr, --skip-text, --redo-ocr.")
|
||||
# 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_ocr compatibility
|
||||
if options.redo_ocr:
|
||||
# Validate redo mode compatibility
|
||||
if options.mode == ProcessingMode.redo:
|
||||
if options.deskew or options.clean_final or options.remove_background:
|
||||
raise ValueError(
|
||||
"--redo-ocr is not currently compatible with --deskew, "
|
||||
"--clean-final, and --remove-background"
|
||||
"--redo-ocr (or --mode redo) is not currently compatible with "
|
||||
"--deskew, --clean-final, and --remove-background"
|
||||
)
|
||||
|
||||
# Validate output type compatibility
|
||||
|
||||
+23
-12
@@ -269,13 +269,16 @@ def create_options(
|
||||
# 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
|
||||
legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'}
|
||||
|
||||
# Known extra attributes that should be preserved
|
||||
known_extra = {'progress_bar', 'plugins'}
|
||||
|
||||
for key in list(options_kwargs.keys()):
|
||||
if key not in ocr_fields and key not in known_extra:
|
||||
extra_attrs[key] = options_kwargs.pop(key)
|
||||
if key in ocr_fields or key in legacy_mode_flags or key in known_extra:
|
||||
continue
|
||||
extra_attrs[key] = options_kwargs.pop(key)
|
||||
|
||||
# Create OCROptions directly
|
||||
try:
|
||||
@@ -311,9 +314,10 @@ def ocr( # noqa: D417
|
||||
unpaper_args: str | None = None,
|
||||
oversample: int | None = None,
|
||||
remove_vectors: bool | None = None,
|
||||
force_ocr: bool | None = None,
|
||||
skip_text: bool | None = None,
|
||||
redo_ocr: bool | None = None,
|
||||
mode: str | None = None,
|
||||
force_ocr: bool | None = None, # Legacy, use mode='force' instead
|
||||
skip_text: bool | None = None, # Legacy, use mode='skip' instead
|
||||
redo_ocr: bool | None = None, # Legacy, use mode='redo' instead
|
||||
skip_big: float | None = None,
|
||||
optimize: int | None = None,
|
||||
jpg_quality: int | None = None,
|
||||
@@ -478,9 +482,10 @@ def _pdf_to_hocr( # noqa: D417
|
||||
unpaper_args: str | None = None,
|
||||
oversample: int | None = None,
|
||||
remove_vectors: bool | None = None,
|
||||
force_ocr: bool | None = None,
|
||||
skip_text: bool | None = None,
|
||||
redo_ocr: bool | None = None,
|
||||
mode: str | None = None,
|
||||
force_ocr: bool | None = None, # Legacy, use mode='force' instead
|
||||
skip_text: bool | None = None, # Legacy, use mode='skip' instead
|
||||
redo_ocr: bool | None = None, # Legacy, use mode='redo' instead
|
||||
skip_big: float | None = None,
|
||||
pages: str | None = None,
|
||||
max_image_mpixels: float | None = None,
|
||||
@@ -562,11 +567,14 @@ def _pdf_to_hocr( # noqa: D417
|
||||
# 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
|
||||
legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'}
|
||||
known_extra = {'progress_bar', 'plugins'}
|
||||
|
||||
for key in list(options_kwargs.keys()):
|
||||
if key not in ocr_fields and key not in known_extra:
|
||||
extra_attrs[key] = options_kwargs.pop(key)
|
||||
if key in ocr_fields or key in legacy_mode_flags or key in known_extra:
|
||||
continue
|
||||
extra_attrs[key] = options_kwargs.pop(key)
|
||||
|
||||
with _api_lock:
|
||||
# Set up plugin infrastructure with proper initialization
|
||||
@@ -675,11 +683,14 @@ def _hocr_to_ocr_pdf( # noqa: D417
|
||||
# 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
|
||||
legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'}
|
||||
known_extra = {'progress_bar', 'plugins'}
|
||||
|
||||
for key in list(options_kwargs.keys()):
|
||||
if key not in ocr_fields and key not in known_extra:
|
||||
extra_attrs[key] = options_kwargs.pop(key)
|
||||
if key in ocr_fields or key in legacy_mode_flags or key in known_extra:
|
||||
continue
|
||||
extra_attrs[key] = options_kwargs.pop(key)
|
||||
|
||||
with _api_lock:
|
||||
# Set up plugin infrastructure with proper initialization
|
||||
|
||||
@@ -15,6 +15,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
from ocrmypdf._exec import ghostscript
|
||||
from ocrmypdf._options import ProcessingMode
|
||||
from ocrmypdf.exceptions import MissingDependencyError
|
||||
from ocrmypdf.subprocess import check_external_program
|
||||
|
||||
@@ -117,15 +118,15 @@ def check_options(options):
|
||||
"supported. Please upgrade to a newer version."
|
||||
)
|
||||
if Version('10.0.0') <= gs_version < Version('10.02.1') and (
|
||||
options.skip_text or options.redo_ocr
|
||||
options.mode in (ProcessingMode.skip, ProcessingMode.redo)
|
||||
):
|
||||
raise MissingDependencyError(
|
||||
f"Ghostscript 10.0.0 through 10.02.0 (your version: {gs_version}) "
|
||||
"contain serious regressions that corrupt PDFs with existing text, "
|
||||
"such as those processed using --skip-text or --redo-ocr. "
|
||||
"Please upgrade to a "
|
||||
"newer version, or use --output-type pdf to avoid Ghostscript, or "
|
||||
"use --force-ocr to discard existing text."
|
||||
"such as those processed using --skip-text or --redo-ocr "
|
||||
"(or --mode skip/redo). Please upgrade to a newer version, or use "
|
||||
"--output-type pdf to avoid Ghostscript, or use --force-ocr "
|
||||
"(or --mode force) to discard existing text."
|
||||
)
|
||||
if gs_version >= Version('10.6.0'):
|
||||
log.warning(
|
||||
|
||||
+25
-4
@@ -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
|
||||
from ocrmypdf._options import OCROptions, ProcessingMode
|
||||
from ocrmypdf._plugin_manager import OcrmypdfPluginManager
|
||||
from ocrmypdf._version import __version__ as _VERSION
|
||||
|
||||
@@ -308,12 +308,25 @@ Online documentation is located at:
|
||||
)
|
||||
|
||||
ocrsettings = parser.add_argument_group("OCR options", "Control how OCR is applied")
|
||||
ocrsettings.add_argument(
|
||||
'-m',
|
||||
'--mode',
|
||||
choices=[mode.value for mode in ProcessingMode],
|
||||
default=ProcessingMode.default.value,
|
||||
help="Processing mode for pages with existing text. "
|
||||
"'default' errors if text is found. "
|
||||
"'force' rasterizes all content and runs OCR (same as --force-ocr). "
|
||||
"'skip' skips pages with existing text (same as --skip-text). "
|
||||
"'redo' re-OCRs pages, replacing old invisible text (same as --redo-ocr).",
|
||||
)
|
||||
# Legacy flags for backward compatibility - these set the mode internally
|
||||
ocrsettings.add_argument(
|
||||
'-f',
|
||||
'--force-ocr',
|
||||
action='store_true',
|
||||
help="Rasterize any text or vector objects on each page, apply OCR, and "
|
||||
"save the rastered output (this rewrites the PDF)",
|
||||
"save the rastered output (this rewrites the PDF). "
|
||||
"Equivalent to --mode force.",
|
||||
)
|
||||
ocrsettings.add_argument(
|
||||
'-s',
|
||||
@@ -321,7 +334,8 @@ Online documentation is located at:
|
||||
action='store_true',
|
||||
help="Skip OCR on any pages that already contain text, but include the "
|
||||
"page in final output; useful for PDFs that contain a mix of "
|
||||
"images, text pages, and/or previously OCRed pages",
|
||||
"images, text pages, and/or previously OCRed pages. "
|
||||
"Equivalent to --mode skip.",
|
||||
)
|
||||
ocrsettings.add_argument(
|
||||
'--redo-ocr',
|
||||
@@ -329,7 +343,8 @@ Online documentation is located at:
|
||||
help="Attempt to detect and remove the hidden OCR layer from files that "
|
||||
"were previously OCRed with OCRmyPDF or another program. Apply OCR "
|
||||
"to text found in raster images. Existing visible text objects will "
|
||||
"not be changed. If there is no existing OCR, OCR will be added.",
|
||||
"not be changed. If there is no existing OCR, OCR will be added. "
|
||||
"Equivalent to --mode redo.",
|
||||
)
|
||||
ocrsettings.add_argument(
|
||||
'--skip-big',
|
||||
@@ -468,9 +483,15 @@ def namespace_to_options(ns) -> OCROptions:
|
||||
known_fields = {}
|
||||
extra_attrs = {}
|
||||
|
||||
# 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:
|
||||
known_fields[key] = value
|
||||
elif key in legacy_mode_flags:
|
||||
# Pass legacy flags to OCROptions for conversion to mode
|
||||
known_fields[key] = value
|
||||
else:
|
||||
extra_attrs[key] = value
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_tesseract_not_installed(caplog):
|
||||
|
||||
|
||||
def test_lossless_redo():
|
||||
with pytest.raises(ValueError, match="--redo-ocr is not currently compatible"):
|
||||
with pytest.raises(ValueError, match="--redo-ocr.*is not currently compatible"):
|
||||
make_ocr_opts(redo_ocr=True, deskew=True)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user