diff --git a/copienator/commands/cutleft.py b/copienator/commands/cutleft.py index 97966b6..dc60125 100644 --- a/copienator/commands/cutleft.py +++ b/copienator/commands/cutleft.py @@ -384,6 +384,7 @@ def run( target: Path, *, fullpage: bool = False, + no_visual_check: bool = False, marked: bool = False, ) -> ExitCode: files = marked_copy_paths(workspace) if marked else _selected_files(workspace, target) @@ -392,6 +393,30 @@ def run( return ExitCode.SUCCESS workspace.cutleft_dir.mkdir(parents=True, exist_ok=True) _get_pdf_pages_cached.cache_clear() + if fullpage and no_visual_check: + had_errors = False + for pdf_path in files: + result = process_single_pdf(pdf_path, max_per_file=1) + if result is None: + mark_copy_error( + workspace, + pdf_path, + "Échec de la conversion en pages entières", + ) + had_errors = True + continue + try: + save_results(result, pdf_path, workspace.cutleft_dir) + clear_copy_error(workspace, pdf_path) + except Exception as exc: # noqa: BLE001 - continue with remaining copies + mark_copy_error( + workspace, + pdf_path, + f"Échec de l’enregistrement : {exc}", + ) + print(f"[Copie signalée] {pdf_path.name}: {exc}") + had_errors = True + return ExitCode.PARTIAL if had_errors else ExitCode.SUCCESS reviewer = ImageReviewer( files, workspace.cutleft_dir, @@ -410,6 +435,11 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Use each complete page instead of cropping the label margin", ) + parser.add_argument( + "--no-visual-check", + action="store_true", + help="Save full pages without opening the visual reviewer (requires --fullpage)", + ) return parser @@ -418,7 +448,13 @@ def main(argv: Sequence[str] | None = None) -> int: def handle(args: argparse.Namespace) -> ExitCode: workspace, target = workspace_from_target(args) - return run(workspace, target, fullpage=args.fullpage, marked=args.marked) + return run( + workspace, + target, + fullpage=args.fullpage, + no_visual_check=args.no_visual_check, + marked=args.marked, + ) return execute(parser, argv, handle) diff --git a/copienator_gui/workflow.py b/copienator_gui/workflow.py index f135b9b..7ef2170 100644 --- a/copienator_gui/workflow.py +++ b/copienator_gui/workflow.py @@ -285,6 +285,13 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]: "--fullpage", help="Désactive la découpe habituelle de la marge gauche et transmet chaque page entière à la détection des labels.", ), + ArgumentSpec( + "no_visual_check", + "et ne pas vérifier visuellement", + "bool", + "--no-visual-check", + help="Avec « Toujours utiliser la page entière », enregistre directement les images sans ouvrir la fenêtre de vérification.", + ), ArgumentSpec( "marked", "Copies signalées uniquement", diff --git a/tests/test_gui_convenience.py b/tests/test_gui_convenience.py index 63d0afc..1cb98d9 100644 --- a/tests/test_gui_convenience.py +++ b/tests/test_gui_convenience.py @@ -51,6 +51,24 @@ class GuiConvenienceTests(unittest.TestCase): start.assert_not_called() self.assertEqual(self.app.state_store.step(step.id)["status"], "success") + def test_cutleft_visual_check_option_is_below_and_off_by_default(self): + step = self.app.step_by_id["cutleft"] + self.app.current_step = step + self.app._render_step() + + labels = { + child.cget("text"): child + for child in self.app.form.winfo_children() + if isinstance(child, ttk.Label) + } + fullpage = labels["Toujours utiliser la page entière ⓘ"] + no_visual_check = labels["et ne pas vérifier visuellement ⓘ"] + self.assertGreater( + int(no_visual_check.grid_info()["row"]), + int(fullpage.grid_info()["row"]), + ) + self.assertFalse(self.app.arg_vars["no_visual_check"].get()) + @staticmethod def _label_texts(widget): return [ diff --git a/tests/test_gui_core.py b/tests/test_gui_core.py index 4b2b3e4..6617ccf 100644 --- a/tests/test_gui_core.py +++ b/tests/test_gui_core.py @@ -566,7 +566,11 @@ class StandardCliTests(unittest.TestCase): "cutleft": ( "cutleft", "default", - {"target": evaluation, "fullpage": True}, + { + "target": evaluation, + "fullpage": True, + "no_visual_check": True, + }, ), "splitting_int": ( "splitting", @@ -757,6 +761,20 @@ class StandardCliTests(unittest.TestCase): self.assertEqual(output_dir, evaluation / "Cutleft") self.assertEqual(reviewer.call_args.kwargs["default_max_per_file"], 1) + result = (Mock(), [Mock()], {}) + with patch.object(module, "ImageReviewer") as reviewer, patch.object( + module, "process_single_pdf", return_value=result + ) as process, patch.object(module, "save_results") as save: + self.assertEqual( + module.main( + [str(copy_pdf), "--fullpage", "--no-visual-check"] + ), + 0, + ) + reviewer.assert_not_called() + process.assert_called_once_with(copy_pdf, max_per_file=1) + save.assert_called_once_with(result, copy_pdf, evaluation / "Cutleft") + def test_cutleft_atomic_save_removes_only_obsolete_copy_outputs(self) -> None: module = self.modules["cutleft"] with tempfile.TemporaryDirectory() as directory: diff --git a/tests/test_marked_copies.py b/tests/test_marked_copies.py index b5e6093..8c79738 100644 --- a/tests/test_marked_copies.py +++ b/tests/test_marked_copies.py @@ -4,7 +4,7 @@ import threading import unittest from pathlib import Path from queue import Queue -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch from PIL import Image @@ -157,6 +157,36 @@ class MarkedCopiesTests(unittest.TestCase): self.assertEqual(cutleft.main([str(self.workspace.root), "--marked"]), expected) self.assertEqual(reviewer.call_args.args[0], [self.files[1]]) + def test_fullpage_processes_every_copy_without_opening_the_reviewer(self): + for path in self.files: + mark_copy_error(self.workspace, path, "Review needed") + image = Image.new("RGB", (8, 8), "white") + result = (image, [image], {"columns_per_file": [1]}) + + with patch.object( + cutleft, "process_single_pdf", side_effect=[result, None] + ) as process, patch.object(cutleft, "save_results") as save, patch.object( + cutleft, "ImageReviewer" + ) as reviewer: + status = cutleft.run( + self.workspace, + self.workspace.root, + fullpage=True, + no_visual_check=True, + ) + + self.assertEqual(status, ExitCode.PARTIAL) + reviewer.assert_not_called() + self.assertEqual( + process.call_args_list, + [ + call(self.files[0], max_per_file=1), + call(self.files[1], max_per_file=1), + ], + ) + save.assert_called_once_with(result, self.files[0], self.workspace.cutleft_dir) + self.assertEqual(set(copy_errors(self.workspace)), {"Copie02.pdf"}) + @unittest.skipUnless(os.environ.get("DISPLAY"), "Tk requires a display") class MarkedCopiesGuiTests(unittest.TestCase):