From 5b9d6f979ed223e65e451db191bc7cf87b49c772 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Tue, 10 Mar 2026 01:58:57 -0700 Subject: [PATCH] Add --no-overwrite / -n option to prevent overwriting output files Fixes #1642. Adds an early check in check_requested_output_file() that raises OutputFileAccessError (exit code 5) if the destination file already exists and --no-overwrite is set. The option is wired through CLI, OcrOptions, and the Python API. --- src/ocrmypdf/_options.py | 3 +++ src/ocrmypdf/_validation.py | 11 +++++++++++ src/ocrmypdf/api.py | 2 ++ src/ocrmypdf/cli.py | 14 ++++++++++++-- uv.lock | 2 +- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/ocrmypdf/_options.py b/src/ocrmypdf/_options.py index e686ab92..275483df 100644 --- a/src/ocrmypdf/_options.py +++ b/src/ocrmypdf/_options.py @@ -188,6 +188,9 @@ class OcrOptions(BaseModel): """Compatibility alias for jpg_quality.""" self.jpg_quality = value + # Output behavior + no_overwrite: bool = False + # Advanced options max_image_mpixels: float = 250.0 pdf_renderer: str = 'auto' diff --git a/src/ocrmypdf/_validation.py b/src/ocrmypdf/_validation.py index 06927daa..a662ee69 100644 --- a/src/ocrmypdf/_validation.py +++ b/src/ocrmypdf/_validation.py @@ -217,6 +217,17 @@ def check_requested_output_file(options: OcrOptions) -> None: f"Output file location ({options.output_file}) is not a writable file." ) + if ( + options.no_overwrite + and not hasattr(options.output_file, 'writable') + and options.output_file != '-' + and Path(str(options.output_file)).exists() + ): + raise OutputFileAccessError( + f"Output file already exists: {options.output_file}\n" + "To overwrite it, omit the --no-overwrite / -n option." + ) + def report_output_file_size( options: OcrOptions, diff --git a/src/ocrmypdf/api.py b/src/ocrmypdf/api.py index 605b8b1c..1bd30995 100644 --- a/src/ocrmypdf/api.py +++ b/src/ocrmypdf/api.py @@ -440,6 +440,7 @@ def ocr( continue_on_soft_render_error: bool | None = None, invalidate_digital_signatures: bool | None = None, tagged_pdf_mode: str | None = None, + no_overwrite: bool | None = None, plugins: Iterable[Path | str] | None = None, plugin_manager: OcrmypdfPluginManager | None = None, keep_temporary_files: bool | None = None, @@ -502,6 +503,7 @@ def ocr( # noqa: D417 continue_on_soft_render_error: bool | None = None, invalidate_digital_signatures: bool | None = None, tagged_pdf_mode: str | None = None, + no_overwrite: bool | None = None, plugins: Iterable[Path | str] | None = None, plugin_manager: OcrmypdfPluginManager | None = None, keep_temporary_files: bool | None = None, diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py index e590dbac..a4bbf2ff 100644 --- a/src/ocrmypdf/cli.py +++ b/src/ocrmypdf/cli.py @@ -137,8 +137,9 @@ Online documentation is located at: 'output_file', metavar="output_pdf", help="Output searchable PDF file (or '-' to write to standard output). " - "Existing files will be overwritten. If same as input file, the " - "input file will be updated only if processing is successful.", + "Existing files will be overwritten (use --no-overwrite to prevent this). " + "If same as input file, the input file will be updated only if " + "processing is successful.", ) parser.add_argument( '-l', @@ -190,6 +191,15 @@ Online documentation is located at: "may not both use stdout at the same time.", ) + parser.add_argument( + '-n', + '--no-overwrite', + action='store_true', + default=False, + help="If the output file already exists, exit with an error instead of " + "overwriting it.", + ) + parser.add_argument( '--version', action='version', diff --git a/uv.lock b/uv.lock index 82dc47ce..0a993f95 100644 --- a/uv.lock +++ b/uv.lock @@ -1283,7 +1283,7 @@ wheels = [ [[package]] name = "ocrmypdf" -version = "17.2.0" +version = "17.3.0" source = { editable = "." } dependencies = [ { name = "deprecation" },