102 lines
3.2 KiB
Python
102 lines
3.2 KiB
Python
# 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
|