diff --git a/src/ocrmypdf/_options.py b/src/ocrmypdf/_options.py index d771facf..38590090 100644 --- a/src/ocrmypdf/_options.py +++ b/src/ocrmypdf/_options.py @@ -161,6 +161,8 @@ class OCROptions(BaseModel): tesseract_non_ocr_timeout: float | None = None tesseract_downsample_above: int = 32767 tesseract_downsample_large_images: bool | None = None + user_words: str | None = None + user_patterns: str | None = None # Legacy ghostscript options (for backward compatibility) pdfa_image_compression: str | None = None diff --git a/src/ocrmypdf/_plugin_registry.py b/src/ocrmypdf/_plugin_registry.py index 3f295624..d7fd9eaf 100644 --- a/src/ocrmypdf/_plugin_registry.py +++ b/src/ocrmypdf/_plugin_registry.py @@ -76,3 +76,81 @@ class PluginOptionRegistry: def clear_cache(self) -> None: """Clear the extended model cache.""" self._extended_model_cache = None + + def map_legacy_options(self, options: dict) -> dict: + """Map legacy flat options to nested plugin options. + + This method helps with backward compatibility by mapping flat option + names (like 'tesseract_timeout') to nested plugin option structures. + + Args: + options: Dictionary of flat options + + Returns: + Dictionary with both legacy flat options and nested plugin options + """ + result = options.copy() + + # Map tesseract options + if 'tesseract' in self._option_models: + tesseract_options = {} + tesseract_fields = self._option_models['tesseract'].model_fields.keys() + + for field in tesseract_fields: + legacy_key = f'tesseract_{field}' + if legacy_key in options: + tesseract_options[field] = options[legacy_key] + + if tesseract_options: + result['tesseract'] = tesseract_options + + # Map optimize options + if 'optimize' in self._option_models: + optimize_options = {} + optimize_fields = self._option_models['optimize'].model_fields.keys() + + for field in optimize_fields: + if field in options: + optimize_options[field] = options[field] + # Handle special case for optimize level + elif field == 'level' and 'optimize' in options: + optimize_options[field] = options['optimize'] + + if optimize_options: + result['optimize'] = optimize_options + + # Map ghostscript options + if 'ghostscript' in self._option_models: + ghostscript_options = {} + ghostscript_fields = self._option_models['ghostscript'].model_fields.keys() + + for field in ghostscript_fields: + if field in options: + ghostscript_options[field] = options[field] + + if ghostscript_options: + result['ghostscript'] = ghostscript_options + + return result + + def validate_plugin_options(self, options: dict) -> dict: + """Validate plugin options using their registered models. + + Args: + options: Dictionary containing plugin options + + Returns: + Dictionary with validated plugin options + + Raises: + ValidationError: If any plugin options are invalid + """ + validated = {} + + for namespace, model_class in self._option_models.items(): + if namespace in options: + # Validate using the plugin's model + plugin_options = model_class(**options[namespace]) + validated[namespace] = plugin_options.model_dump() + + return validated diff --git a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py index 9b53f2a4..e9ca80d9 100644 --- a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py +++ b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py @@ -58,6 +58,12 @@ class TesseractOptions(BaseModel): description="Downsample images larger than this pixel size", ), ] = 32767 + user_words: Annotated[ + str | None, Field(description="Path to Tesseract user words file") + ] = None + user_patterns: Annotated[ + str | None, Field(description="Path to Tesseract user patterns file") + ] = None @classmethod def add_arguments_to_parser(cls, parser, namespace: str = 'tesseract'): @@ -182,6 +188,22 @@ class TesseractOptions(BaseModel): ), ) + tess.add_argument( + '--user-words', + metavar='FILE', + dest='user_words', + help="Specify the location of the Tesseract user words file. This is a " + "list of words Tesseract should consider while performing OCR in " + "addition to its standard language dictionaries. This can improve " + "OCR quality especially for specialized and technical documents.", + ) + tess.add_argument( + '--user-patterns', + metavar='FILE', + dest='user_patterns', + help="Specify the location of the Tesseract user patterns file.", + ) + @hookimpl def register_options(): @@ -191,25 +213,9 @@ def register_options(): @hookimpl def add_options(parser): - # Use the model's CLI generation method + # Use the model's CLI generation method - it now handles all Tesseract options TesseractOptions.add_arguments_to_parser(parser) - # Add user words and patterns (these are not part of TesseractOptions model yet) - tess = parser.add_argument_group("Tesseract", "Advanced control of Tesseract OCR") - tess.add_argument( - '--user-words', - metavar='FILE', - help="Specify the location of the Tesseract user words file. This is a " - "list of words Tesseract should consider while performing OCR in " - "addition to its standard language dictionaries. This can improve " - "OCR quality especially for specialized and technical documents.", - ) - tess.add_argument( - '--user-patterns', - metavar='FILE', - help="Specify the location of the Tesseract user patterns file.", - ) - @hookimpl def check_options(options):