# SPDX-FileCopyrightText: 2026 James R. Barlow # SPDX-License-Identifier: AGPL-3.0-or-later """Validated OCR options for the batch web interface. The whole point of this module is that *nothing* the browser sends is ever interpolated into a command line unchecked. Every option is either a fixed enum, a bounded integer, or a language code drawn from the set of language packs actually installed in the image. """ from __future__ import annotations from enum import StrEnum from pydantic import BaseModel, ConfigDict, Field, field_validator from webui.config import installed_languages class Mode(StrEnum): """What to do about pages that already contain text.""" normal = "normal" skip_text = "skip-text" force_ocr = "force-ocr" redo_ocr = "redo-ocr" class OutputType(StrEnum): """Requested output conformance level.""" pdfa = "pdfa" pdfa_1 = "pdfa-1" pdfa_2 = "pdfa-2" pdfa_3 = "pdfa-3" pdf = "pdf" class OcrOptions(BaseModel): """Options applied to every file in a batch.""" model_config = ConfigDict(extra="forbid", use_enum_values=False) languages: list[str] = Field(default_factory=lambda: ["eng"], max_length=8) mode: Mode = Mode.skip_text output_type: OutputType = OutputType.pdfa optimize: int = Field(default=1, ge=0, le=3) deskew: bool = False clean: bool = False rotate_pages: bool = False image_dpi: int = Field(default=300, ge=1, le=5000) @field_validator("languages") @classmethod def _known_languages(cls, value: list[str]) -> list[str]: if not value: return ["eng"] available = set(installed_languages()) unknown = [lang for lang in value if lang not in available] if unknown: raise ValueError( "unknown or uninstalled language(s): " + ", ".join(sorted(unknown)) ) # Preserve caller order (Tesseract weights the first language most) # while dropping duplicates. seen: set[str] = set() ordered = [] for lang in value: if lang not in seen: seen.add(lang) ordered.append(lang) return ordered def to_args(self, *, jobs: int, is_image: bool) -> list[str]: """Render these options as ``ocrmypdf`` command line arguments. Args: jobs: Value for ``--jobs``, OCRmyPDF's internal worker count. is_image: True when the input is an image rather than a PDF, in which case ``--image-dpi`` is meaningful. """ args = [ f"--language={'+'.join(self.languages)}", f"--output-type={self.output_type.value}", f"--optimize={self.optimize}", f"--jobs={jobs}", ] if self.mode is not Mode.normal: args.append(f"--{self.mode.value}") if self.deskew: args.append("--deskew") if self.clean: args.append("--clean") if self.rotate_pages: args.append("--rotate-pages") if is_image: args.append(f"--image-dpi={self.image_dpi}") # A soft render error on one page shouldn't sink an unattended batch. args.append("--continue-on-soft-render-error") return args